diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 051cddc..4a6d656 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -1,1221 +1,327 @@ -use crate::config::Language; -use anyhow::{Context, Result, bail}; -use async_trait::async_trait; -use std::time::Duration; - -pub mod anthropic; -pub mod deepseek; -pub mod kimi; -pub mod ollama; -pub mod openai; -pub mod openrouter; -pub mod thinking; - -pub use anthropic::AnthropicClient; -pub use deepseek::DeepSeekClient; -pub use kimi::KimiClient; -pub use ollama::OllamaClient; -pub use openai::OpenAiClient; -pub use openrouter::OpenRouterClient; - -/// LLM provider trait -#[async_trait] -pub trait LlmProvider: Send + Sync { - /// Generate text from prompt - async fn generate(&self, prompt: &str) -> Result; - - /// Generate with system prompt - async fn generate_with_system(&self, system: &str, user: &str) -> Result; - - /// Check if provider is available - async fn is_available(&self) -> bool; - - /// Get provider name - fn name(&self) -> &str; -} - -/// LLM client that wraps different providers -pub struct LlmClient { - provider: Box, - config: LlmClientConfig, -} - -#[derive(Debug, Clone)] -pub struct LlmClientConfig { - pub max_tokens: u32, - pub temperature: f32, - pub timeout: Duration, - pub thinking_enabled: bool, -} - -impl Default for LlmClientConfig { - fn default() -> Self { - Self { - max_tokens: 500, - temperature: 0.7, - timeout: Duration::from_secs(30), - thinking_enabled: false, - } - } -} - -impl LlmClient { - /// Create LLM client from configuration manager - pub async fn from_config(manager: &crate::config::manager::ConfigManager) -> Result { - Self::from_config_with_think(manager, manager.config().llm.thinking_enabled).await - } - - /// Create LLM client from configuration with explicit thinking override - pub async fn from_config_with_think( - manager: &crate::config::manager::ConfigManager, - thinking_enabled: bool, - ) -> Result { - let config = manager.config(); - let client_config = LlmClientConfig { - max_tokens: config.llm.max_tokens, - temperature: config.llm.temperature, - timeout: Duration::from_secs(config.llm.timeout), - thinking_enabled, - }; - - let provider = config.llm.provider.as_str(); - let model = config.llm.model.as_str(); - let base_url = manager.llm_base_url(); - let api_key = manager.get_api_key(); - - let provider: Box = match provider { - "ollama" => Box::new( - OllamaClient::new(&base_url, model) - .with_max_tokens(client_config.max_tokens) - .with_temperature(client_config.temperature), - ), - "openai" => { - let key = api_key - .as_ref() - .ok_or_else(|| anyhow::anyhow!("OpenAI API key not configured"))?; - let thinking_state = if thinking_enabled { - Some(thinking::create_console_thinking_state()) - } else { - None - }; - let mut client = OpenAiClient::new(&base_url, key, model)? - .with_thinking(thinking_enabled) - .with_max_tokens(client_config.max_tokens) - .with_temperature(client_config.temperature) - .with_timeout(client_config.timeout)?; - if let Some(state) = thinking_state { - client = client.with_thinking_state(state); - } - Box::new(client) - } - "anthropic" => { - let key = api_key - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Anthropic API key not configured"))?; - let thinking_state = if thinking_enabled { - Some(thinking::create_console_thinking_state()) - } else { - None - }; - let budget = config.llm.thinking_budget_tokens.unwrap_or(1024); - let mut client = AnthropicClient::new(key, model)? - .with_thinking(thinking_enabled) - .with_thinking_budget_tokens(budget) - .with_max_tokens(client_config.max_tokens) - .with_temperature(client_config.temperature) - .with_timeout(client_config.timeout)?; - if let Some(state) = thinking_state { - client = client.with_thinking_state(state); - } - Box::new(client) - } - "kimi" => { - let key = api_key - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Kimi API key not configured"))?; - let thinking_state = if thinking_enabled { - Some(thinking::create_console_thinking_state()) - } else { - None - }; - let mut client = KimiClient::with_base_url(key, model, &base_url)? - .with_thinking(thinking_enabled) - .with_max_tokens(client_config.max_tokens) - .with_temperature(client_config.temperature) - .with_timeout(client_config.timeout)?; - if let Some(state) = thinking_state { - client = client.with_thinking_state(state); - } - Box::new(client) - } - "deepseek" => { - let key = api_key - .as_ref() - .ok_or_else(|| anyhow::anyhow!("DeepSeek API key not configured"))?; - let thinking_state = if thinking_enabled { - Some(thinking::create_console_thinking_state()) - } else { - None - }; - let mut client = DeepSeekClient::with_base_url(key, model, &base_url)? - .with_thinking(thinking_enabled) - .with_max_tokens(client_config.max_tokens) - .with_temperature(client_config.temperature) - .with_timeout(client_config.timeout)?; - if let Some(state) = thinking_state { - client = client.with_thinking_state(state); - } - Box::new(client) - } - "openrouter" => { - let key = api_key - .as_ref() - .ok_or_else(|| anyhow::anyhow!("OpenRouter API key not configured"))?; - Box::new( - OpenRouterClient::with_base_url(key, model, &base_url)? - .with_max_tokens(client_config.max_tokens) - .with_temperature(client_config.temperature) - .with_timeout(client_config.timeout)?, - ) - } - _ => bail!("Unknown LLM provider: {}", provider), - }; - - Ok(Self { - provider, - config: client_config, - }) - } - - /// Create with specific provider - pub fn with_provider(provider: Box) -> Self { - Self { - provider, - config: LlmClientConfig::default(), - } - } - - /// Generate commit message from git diff - pub async fn generate_commit_message( - &self, - diff: &str, - format: crate::config::CommitFormat, - language: Language, - template: Option<&str>, - ) -> Result { - let mut system_prompt = get_commit_system_prompt(format, language).to_string(); - - if let Some(tmpl) = template { - system_prompt.push_str(&format!( - "\n\n## Commit Message Template\nFollow this template structure:\n{}", - tmpl - )); - } - - // Add language instruction to the prompt - let language_instruction = match language { - Language::Chinese => "\n\n请用中文生成提交消息。", - Language::Japanese => "\n\n日本語でコミットメッセージを生成してください。", - Language::Korean => "\n\n한국어로 커밋 메시지를 생성하세요.", - Language::Spanish => "\n\nPor favor, genera el mensaje de commit en español.", - Language::French => "\n\nVeuillez générer le message de commit en français.", - Language::German => "\n\nBitte generieren Sie die Commit-Nachricht auf Deutsch.", - Language::English => "", - }; - - let prompt = format!("{}{}", diff, language_instruction); - let response = self - .provider - .generate_with_system(&system_prompt, &prompt) - .await?; - - self.parse_commit_response(&response, format) - } - - /// Generate tag message from commits - pub async fn generate_tag_message( - &self, - version: &str, - commits: &[String], - language: Language, - ) -> Result { - let system_prompt = get_tag_system_prompt(language); - let commits_text = commits.join("\n"); - - // Add language instruction to the prompt - let language_instruction = match language { - Language::Chinese => "\n\n请用中文生成标签消息。", - Language::Japanese => "\n\n日本語でタグメッセージを生成してください。", - Language::Korean => "\n\n한국어로 태그 메시지를 생성하세요.", - Language::Spanish => "\n\nPor favor, genera el mensaje de etiqueta en español.", - Language::French => "\n\nVeuillez générer le message de balise en français.", - Language::German => "\n\nBitte generieren Sie die Tag-Nachricht auf Deutsch.", - Language::English => "", - }; - - let prompt = format!( - "Version: {}\n\nCommits:\n{}{}", - version, commits_text, language_instruction - ); - - self.provider - .generate_with_system(system_prompt, &prompt) - .await - } - - /// Generate changelog entry - pub async fn generate_changelog_entry( - &self, - version: &str, - commits: &[(String, String)], // (type, message) - language: Language, - ) -> Result { - let system_prompt = get_changelog_system_prompt(language); - - let commits_text = commits - .iter() - .map(|(t, m)| format!("- [{}] {}", t, m)) - .collect::>() - .join("\n"); - - // Add language instruction to the prompt - let language_instruction = match language { - Language::Chinese => "\n\n请用中文生成变更日志。", - Language::Japanese => "\n\n日本語で変更ログを生成してください。", - Language::Korean => "\n\n한국어로 변경 로그를 생성하세요.", - Language::Spanish => "\n\nPor favor, genera el registro de cambios en español.", - Language::French => "\n\nVeuillez générer le journal des modifications en français.", - Language::German => "\n\nBitte generieren Sie das Changelog auf Deutsch.", - Language::English => "", - }; - - let prompt = format!( - "Version: {}\n\nCommits:\n{}{}", - version, commits_text, language_instruction - ); - - self.provider - .generate_with_system(system_prompt, &prompt) - .await - } - - /// Check if provider is available - pub async fn is_available(&self) -> bool { - self.provider.is_available().await - } - - /// Parse commit response from LLM - fn parse_commit_response( - &self, - response: &str, - format: crate::config::CommitFormat, - ) -> Result { - // Clean markdown code fences from the response - let cleaned = Self::strip_code_fences(response); - - let lines: Vec<&str> = cleaned - .lines() - .map(|l| l.trim()) - .filter(|l| !l.is_empty()) - .collect(); - - if lines.is_empty() { - let preview: String = response.chars().take(200).collect(); - bail!( - "LLM returned empty or whitespace-only response. \ - Raw response preview: '{}'. \ - Hint: If using DeepSeek/Kimi with thinking enabled, \ - the model may have returned reasoning_content only. \ - Try disabling thinking mode or switching models.", - preview - ); - } - - // Find the line most likely to be the commit subject - let first_line = Self::find_commit_subject_line(&lines, format); - - // Parse based on format - match format { - crate::config::CommitFormat::Conventional => { - self.parse_conventional_commit(first_line, &lines, response) - } - crate::config::CommitFormat::Commitlint => { - self.parse_commitlint_commit(first_line, &lines, response) - } - } - } - - /// Remove surrounding markdown code fences (```) from LLM output - fn strip_code_fences(response: &str) -> String { - let mut lines: Vec<&str> = response.lines().collect(); - - // Strip leading fence lines (``` or ```lang) - while lines.first().map_or(false, |l| l.trim().starts_with("```")) { - lines.remove(0); - } - - // Strip trailing fence lines - while lines.last().map_or(false, |l| l.trim() == "```") { - lines.pop(); - } - - lines.join("\n") - } - - /// Find the line that is most likely the commit subject among extracted lines - fn find_commit_subject_line<'a>( - lines: &[&'a str], - format: crate::config::CommitFormat, - ) -> &'a str { - let valid_types = crate::utils::validators::get_commit_types(matches!( - format, - crate::config::CommitFormat::Commitlint - )); - - // First pass: line starting with a known type that also has proper syntax - // (e.g. "type:", "type(scope):", "type!:") - for &line in lines { - let trimmed = line.trim(); - for &t in valid_types { - if let Some(rest) = trimmed.strip_prefix(t) { - if rest.starts_with(':') || rest.starts_with('(') || rest.starts_with("!:") { - return trimmed; - } - } - } - } - - // Second pass: any line containing a colon (generic "prefix: description") - for &line in lines { - if line.contains(':') { - return line.trim(); - } - } - - // Fallback: return the first line as-is - lines[0].trim() - } - - fn parse_conventional_commit( - &self, - first_line: &str, - lines: &[&str], - raw_response: &str, - ) -> Result { - // Parse: type(scope)!: description - let parts: Vec<&str> = first_line.splitn(2, ':').collect(); - if parts.len() != 2 { - let preview: String = raw_response.chars().take(300).collect(); - bail!( - "Invalid conventional commit format: missing colon.\n\ - Parsed subject line: '{}'\n\ - Raw response preview: '{}'\n\ - Expected: [optional scope]: ", - first_line, - preview - ); - } - - let type_part = parts[0]; - let description = parts[1].trim(); - - // Extract type, scope, and breaking indicator - let breaking = type_part.ends_with('!'); - let type_part = type_part.trim_end_matches('!'); - - let (commit_type, scope) = if let Some(start) = type_part.find('(') { - if let Some(end) = type_part.find(')') { - let t = &type_part[..start]; - let s = &type_part[start + 1..end]; - (t.to_string(), Some(s.to_string())) - } else { - bail!("Invalid scope format: missing closing parenthesis"); - } - } else { - (type_part.to_string(), None) - }; - - // Extract body and footer - let (body, footer) = self.extract_body_footer(lines); - - Ok(GeneratedCommit { - commit_type, - scope, - description: description.to_string(), - body, - footer, - breaking, - }) - } - - fn parse_commitlint_commit( - &self, - first_line: &str, - lines: &[&str], - raw_response: &str, - ) -> Result { - // Similar parsing but with commitlint rules - let parts: Vec<&str> = first_line.splitn(2, ':').collect(); - if parts.len() != 2 { - let preview: String = raw_response.chars().take(300).collect(); - bail!( - "Invalid commit format: missing colon.\n\ - Parsed subject line: '{}'\n\ - Raw response preview: '{}'\n\ - Expected: [optional scope]: ", - first_line, - preview - ); - } - - let type_part = parts[0]; - let subject = parts[1].trim(); - - let (commit_type, scope) = if let Some(start) = type_part.find('(') { - if let Some(end) = type_part.find(')') { - let t = &type_part[..start]; - let s = &type_part[start + 1..end]; - (t.to_string(), Some(s.to_string())) - } else { - (type_part.to_string(), None) - } - } else { - (type_part.to_string(), None) - }; - - let (body, footer) = self.extract_body_footer(&lines); - - Ok(GeneratedCommit { - commit_type, - scope, - description: subject.to_string(), - body, - footer, - breaking: false, - }) - } - - fn extract_body_footer(&self, lines: &[&str]) -> (Option, Option) { - if lines.len() <= 1 { - return (None, None); - } - - let rest: Vec<&str> = lines[1..] - .iter() - .skip_while(|l| l.trim().is_empty()) - .copied() - .collect(); - - if rest.is_empty() { - return (None, None); - } - - // Look for footer markers - let footer_markers = [ - "BREAKING CHANGE:", - "Closes", - "Fixes", - "Refs", - "Co-authored-by:", - ]; - - let mut body_lines = vec![]; - let mut footer_lines = vec![]; - let mut in_footer = false; - - for line in &rest { - if footer_markers.iter().any(|m| line.starts_with(m)) { - in_footer = true; - } - - if in_footer { - footer_lines.push(*line); - } else { - body_lines.push(*line); - } - } - - let body = if body_lines.is_empty() { - None - } else { - Some(body_lines.join("\n")) - }; - - let footer = if footer_lines.is_empty() { - None - } else { - Some(footer_lines.join("\n")) - }; - - (body, footer) - } -} - -/// Generated commit structure -#[derive(Debug, Clone)] -pub struct GeneratedCommit { - pub commit_type: String, - pub scope: Option, - pub description: String, - pub body: Option, - pub footer: Option, - pub breaking: bool, -} - -impl GeneratedCommit { - /// Format as conventional commit - pub fn to_conventional(&self) -> String { - crate::utils::formatter::format_conventional_commit( - &self.commit_type, - self.scope.as_deref(), - &self.description, - self.body.as_deref(), - self.footer.as_deref(), - self.breaking, - ) - } - - /// Format as commitlint commit - pub fn to_commitlint(&self) -> String { - crate::utils::formatter::format_commitlint_commit( - &self.commit_type, - self.scope.as_deref(), - &self.description, - self.body.as_deref(), - self.footer.as_deref(), - None, - ) - } -} - -/// HTTP client helper -pub(crate) fn create_http_client(timeout: Duration) -> Result { - reqwest::Client::builder() - .timeout(timeout) - .build() - .context("Failed to create HTTP client") -} - -/// Get commit system prompt based on format and language -fn get_commit_system_prompt( - format: crate::config::CommitFormat, - language: Language, -) -> &'static str { - match (format, language) { - (crate::config::CommitFormat::Conventional, Language::Chinese) => { - CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ZH - } - (crate::config::CommitFormat::Conventional, Language::Japanese) => { - CONVENTIONAL_COMMIT_SYSTEM_PROMPT_JA - } - (crate::config::CommitFormat::Conventional, Language::Korean) => { - CONVENTIONAL_COMMIT_SYSTEM_PROMPT_KO - } - (crate::config::CommitFormat::Conventional, Language::Spanish) => { - CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ES - } - (crate::config::CommitFormat::Conventional, Language::French) => { - CONVENTIONAL_COMMIT_SYSTEM_PROMPT_FR - } - (crate::config::CommitFormat::Conventional, Language::German) => { - CONVENTIONAL_COMMIT_SYSTEM_PROMPT_DE - } - (crate::config::CommitFormat::Conventional, _) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT, - (crate::config::CommitFormat::Commitlint, Language::Chinese) => COMMITLINT_SYSTEM_PROMPT_ZH, - (crate::config::CommitFormat::Commitlint, Language::Japanese) => { - COMMITLINT_SYSTEM_PROMPT_JA - } - (crate::config::CommitFormat::Commitlint, Language::Korean) => COMMITLINT_SYSTEM_PROMPT_KO, - (crate::config::CommitFormat::Commitlint, Language::Spanish) => COMMITLINT_SYSTEM_PROMPT_ES, - (crate::config::CommitFormat::Commitlint, Language::French) => COMMITLINT_SYSTEM_PROMPT_FR, - (crate::config::CommitFormat::Commitlint, Language::German) => COMMITLINT_SYSTEM_PROMPT_DE, - (crate::config::CommitFormat::Commitlint, _) => COMMITLINT_SYSTEM_PROMPT, - } -} - -fn get_tag_system_prompt(language: Language) -> &'static str { - match language { - Language::Chinese => TAG_MESSAGE_SYSTEM_PROMPT_ZH, - Language::Japanese => TAG_MESSAGE_SYSTEM_PROMPT_JA, - Language::Korean => TAG_MESSAGE_SYSTEM_PROMPT_KO, - Language::Spanish => TAG_MESSAGE_SYSTEM_PROMPT_ES, - Language::French => TAG_MESSAGE_SYSTEM_PROMPT_FR, - Language::German => TAG_MESSAGE_SYSTEM_PROMPT_DE, - _ => TAG_MESSAGE_SYSTEM_PROMPT, - } -} - -fn get_changelog_system_prompt(language: Language) -> &'static str { - match language { - Language::Chinese => CHANGELOG_SYSTEM_PROMPT_ZH, - Language::Japanese => CHANGELOG_SYSTEM_PROMPT_JA, - Language::Korean => CHANGELOG_SYSTEM_PROMPT_KO, - Language::Spanish => CHANGELOG_SYSTEM_PROMPT_ES, - Language::French => CHANGELOG_SYSTEM_PROMPT_FR, - Language::German => CHANGELOG_SYSTEM_PROMPT_DE, - _ => CHANGELOG_SYSTEM_PROMPT, - } -} - -// System prompts for LLM - -const CONVENTIONAL_COMMIT_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates conventional commit messages. - -Analyze the git diff provided and generate a commit message following the Conventional Commits specification. - -Format: [optional scope]: - -Types: -- feat: A new feature -- fix: A bug fix -- docs: Documentation only changes -- style: Changes that don't affect code meaning (formatting, semicolons, etc.) -- refactor: Code change that neither fixes a bug nor adds a feature -- perf: Code change that improves performance -- test: Adding or correcting tests -- build: Changes to build system or dependencies -- ci: Changes to CI configuration -- chore: Other changes that don't modify src or test files -- revert: Reverts a previous commit - -Rules: -1. Use lowercase for type and scope -2. Keep description under 100 characters -3. Use imperative mood ("add" not "added") -4. Don't capitalize first letter -5. No period at the end -6. Include scope if the change is specific to a module/component - -Output ONLY the commit message, nothing else. -"#; - -const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ZH: &str = r#"你是一个生成符合 Conventional Commits 规范的提交消息的助手。 - -分析提供的 git diff,并生成符合 Conventional Commits 规范的提交消息。 - -格式: [可选作用域]: <描述> - -类型: -- feat: 新功能 -- fix: 修复错误 -- docs: 仅文档更改 -- style: 不影响代码含义的更改(格式化、分号等) -- refactor: 既不修复错误也不添加功能的代码更改 -- perf: 提高性能的代码更改 -- test: 添加或更正测试 -- build: 更改构建系统或依赖项 -- ci: 更改 CI 配置 -- chore: 其他不修改 src 或测试文件的更改 -- revert: 撤销之前的提交 - -规则: -1. 类型和小写使用小写 -2. 描述保持在 100 个字符以内 -3. 使用祈使语气("添加"而不是"已添加") -4. 不要大写首字母 -5. 结尾不要句号 -6. 如果更改特定于模块/组件,请包含作用域 -7. 仅输出提交消息,不要输出其他内容。 -"#; - -const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_JA: &str = r#"あなたはConventional Commits仕様に従ったコミットメッセージを生成するアシスタントです。 - -提供されたgit diffを分析し、Conventional Commits仕様に従ったコミットメッセージを生成してください。 - -形式: [オプションのスコープ]: <説明> - -タイプ: -- feat: 新機能 -- fix: バグ修正 -- docs: ドキュメントのみの変更 -- style: コードの意味に影響しない変更(フォーマット、セミコロンなど) -- refactor: バグ修正や機能追加を伴わないコード変更 -- perf: パフォーマンスを向上させるコード変更 -- test: テストの追加または修正 -- build: ビルドシステムまたは依存関係の変更 -- ci: CI設定の変更 -- chore: srcやテストファイルを変更しないその他の変更 -- revert: 以前のコミットを取り消す - -ルール: -1. タイプとスコープは小文字を使用 -2. 説明は100文字以内にする -3. 命令形を使用する("追加"ではなく"追加する") -4. 先頭を大文字にしない -5. 最後にピリオドを付けない -6. 変更がモジュール/コンポーネントに固有の場合はスコープを含める -7. コミットメッセージのみを出力し、それ以外は出力しないでください。 -"#; - -const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_KO: &str = r#"당신은 Conventional Commits 사양에 따른 커밋 메시지를 생성하는 도우미입니다. - -제공된 git diff를 분석하고 Conventional Commits 사양에 따른 커밋 메시지를 생성하세요. - -형식: [선택적 범위]: <설명> - -유형: -- feat: 새 기능 -- fix: 버그 수정 -- docs: 문서 변경만 -- style: 코드 의미에 영향을 주지 않는 변경(서식, 세미콜론 등) -- refactor: 버그를 수정하거나 기능을 추가하지 않는 코드 변경 -- perf: 성능을 향상시키는 코드 변경 -- test: 테스트 추가 또는 수정 -- build: 빌드 시스템 또는 종속성 변경 -- ci: CI 구성 변경 -- chore: src 또는 테스트 파일을 수정하지 않는 기타 변경 -- revert: 이전 커밋 되돌리기 - -규칙: -1. 유형과 범위는 소문자 사용 -2. 설명은 100자 이내로 유지 -3. 명령형 사용("추가"가 아닌 "추가하다") -4. 첫 글자 대문자화하지 않음 -5. 끝에 마침표 사용하지 않음 -6. 변경 사항이 모듈/구성 요소에 특정한 경우 범위 포함 -7. 커밋 메시지만 출력하고 다른 내용은 출력하지 마세요. -"#; - -const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ES: &str = r#"Eres un asistente que genera mensajes de commit siguiendo la especificación Conventional Commits. - -Analiza el diff de git proporcionado y genera un mensaje de commit siguiendo la especificación Conventional Commits. - -Formato: [alcance opcional]: - -Tipos: -- feat: Una nueva característica -- fix: Una corrección de error -- docs: Solo cambios en documentación -- style: Cambios que no afectan el significado del código (formato, punto y coma, etc.) -- refactor: Cambio de código que no corrige un error ni agrega una característica -- perf: Cambio de código que mejora el rendimiento -- test: Agregar o corregir pruebas -- build: Cambios en el sistema de construcción o dependencias -- ci: Cambios en la configuración de CI -- chore: Otros cambios que no modifican archivos src o de prueba -- revert: Revierte un commit anterior - -Reglas: -1. Usa minúsculas para tipo y alcance -2. Mantén la descripción bajo 100 caracteres -3. Usa modo imperativo ("agregar" no "agregado") -4. No capitalices la primera letra -5. Sin punto al final -6. Incluye alcance si el cambio es específico de un módulo/componente -7. Genera SOLO el mensaje de commit, nada más. -"#; - -const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_FR: &str = r#"Vous êtes un assistant qui génère des messages de commit suivant la spécification Conventional Commits. - -Analysez le diff git fourni et générez un message de commit suivant la spécification Conventional Commits. - -Format: [portée optionnelle]: - -Types: -- feat: Une nouvelle fonctionnalité -- fix: Une correction de bug -- docs: Changements de documentation uniquement -- style: Changements qui n'affectent pas la signification du code (formatage, points-virgules, etc.) -- refactor: Changement de code qui ne corrige pas un bug ni n'ajoute une fonctionnalité -- perf: Changement de code qui améliore les performances -- test: Ajout ou correction de tests -- build: Changements du système de build ou des dépendances -- ci: Changements de la configuration CI -- chore: Autres changements qui ne modifient pas les fichiers src ou de test -- revert: Révertit un commit précédent - -Règles: -1. Utilisez des minuscules pour le type et la portée -2. Gardez la description sous 100 caractères -3. Utilisez le mode impératif ("ajouter" non "ajouté") -4. Ne capitalisez pas la première lettre -5. Pas de point à la fin -6. Incluez la portée si le changement est spécifique à un module/composant -7. Générez SEULEMENT le message de commit, rien d'autre. -"#; - -const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_DE: &str = r#"Sie sind ein Assistent, der Commit-Nachrichten gemäß der Conventional Commits-Spezifikation generiert. - -Analysieren Sie den bereitgestellten git diff und generieren Sie eine Commit-Nachricht gemäß der Conventional Commits-Spezifikation. - -Format: [optionaler Bereich]: - -Typen: -- feat: Eine neue Funktion -- fix: Ein Bugfix -- docs: Nur Dokumentationsänderungen -- style: Änderungen, die die Code-Bedeutung nicht beeinflussen (Formatierung, Semikolons usw.) -- refactor: Code-Änderung, die weder einen Bug behebt noch eine Funktion hinzufügt -- perf: Code-Änderung, die die Leistung verbessert -- test: Hinzufügen oder Korrigieren von Tests -- build: Änderungen am Build-System oder Abhängigkeiten -- ci: Änderungen an der CI-Konfiguration -- chore: Andere Änderungen, die src- oder Testdateien nicht ändern -- revert: Setzt einen vorherigen Commit zurück - -Regeln: -1. Verwenden Sie Kleinbuchstaben für Typ und Bereich -2. Halten Sie die Beschreibung unter 100 Zeichen -3. Verwenden Sie den Imperativ ("hinzufügen" nicht "hinzugefügt") -4. Großschreiben Sie den ersten Buchstaben nicht -5. Kein Punkt am Ende -6. Fügen Sie einen Bereich ein, wenn die Änderung spezifisch für ein Modul/Komponente ist -7. Geben Sie NUR die Commit-Nachricht aus, nichts anderes. -"#; - -const COMMITLINT_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates commit messages following @commitlint/config-conventional. - -Analyze the git diff and generate a commit message. - -Format: [optional scope]: - -Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert - -Rules: -1. Subject should not start with uppercase -2. Subject should not end with period -3. Subject should be 4-100 characters -4. Use imperative mood -5. Be concise but descriptive -6. Output ONLY the commit message, nothing else. -"#; - -const COMMITLINT_SYSTEM_PROMPT_ZH: &str = r#"你是一个生成符合 @commitlint/config-conventional 规范的提交消息的助手。 - -分析 git diff 并生成提交消息。 - -格式: [可选作用域]: <主题> - -类型: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert - -规则: -1. 主题不应以大写字母开头 -2. 主题不应以句号结尾 -3. 主题应为 4-100 个字符 -4. 使用祈使语气 -5. 简洁但描述性强 -6. 仅输出提交消息,不要输出其他额外内容。 -"#; - -const COMMITLINT_SYSTEM_PROMPT_JA: &str = r#"あなたは@commitlint/config-conventionalに従ったコミットメッセージを生成するアシスタントです。 - -git diffを分析し、コミットメッセージを生成してください。 - -形式: [オプションのスコープ]: <件名> - -タイプ: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert - -ルール: -1. 件名は大文字で始めないでください -2. 件名はピリオドで終わらないでください -3. 件名は4-100文字である必要があります -4. 命令形を使用してください -5. 簡潔ですが説明的であること -6. コミットメッセージのみを出力し、それ以外は出力しないでください。 -"#; - -const COMMITLINT_SYSTEM_PROMPT_KO: &str = r#"당신은 @commitlint/config-conventional에 따른 커밋 메시지를 생성하는 도우미입니다. - -git diff를 분석하고 커밋 메시지를 생성하세요. - -형식: [선택적 범위]: <제목> - -유형: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert - -규칙: -1. 제목은 대문자로 시작하지 않아야 합니다 -2. 제목은 마침표로 끝나지 않아야 합니다 -3. 제목은 4-100자여야 합니다 -4. 명령형을 사용하세요 -5. 간결하지만 설명적이어야 합니다 -6. 커밋 메시지만 출력하고 다른 내용은 출력하지 마세요. -"#; - -const COMMITLINT_SYSTEM_PROMPT_ES: &str = r#"Eres un asistente que genera mensajes de commit siguiendo @commitlint/config-conventional. - -Analiza el diff de git y genera un mensaje de commit. - -Formato: [alcance opcional]: - -Tipos: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert - -Reglas: -1. El asunto no debe comenzar con mayúscula -2. El asunto no debe terminar con punto -3. El asunto debe tener 4-100 caracteres -4. Usa modo imperativo -5. Sé conciso pero descriptivo -6. Genera SOLO el mensaje de commit, nada más. -"#; - -const COMMITLINT_SYSTEM_PROMPT_FR: &str = r#"Vous êtes un assistant qui génère des messages de commit suivant @commitlint/config-conventional. - -Analysez le diff git et générez un message de commit. - -Format: [portée optionnelle]: - -Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert - -Règles: -1. Le sujet ne doit pas commencer par une majuscule -2. Le sujet ne doit pas se terminer par un point -3. Le sujet doit avoir 4-100 caractères -4. Utilisez le mode impératif -5. Soyez concis mais descriptif -6. Générez SEULEMENT le message de commit, rien d'autre. -"#; - -const COMMITLINT_SYSTEM_PROMPT_DE: &str = r#"Sie sind ein Assistent, der Commit-Nachrichten gemäß @commitlint/config-conventional generiert. - -Analysieren Sie den git diff und generieren Sie eine Commit-Nachricht. - -Format: [optionaler Bereich]: - -Typen: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert - -Regeln: -1. Der Betreff sollte nicht mit einem Großbuchstaben beginnen -2. Der Betreff sollte nicht mit einem Punkt enden -3. Der Betreff sollte 4-100 Zeichen haben -4. Verwenden Sie den Imperativ -5. Seien Sie prägnant aber beschreibend -6. Geben Sie NUR die Commit-Nachricht aus, nichts anderes. -"#; - -const TAG_MESSAGE_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates git tag annotation messages. - -Given a version number and a list of commits, generate a concise but informative tag message. - -The message should: -1. Start with a brief summary of the release -2. Group changes by type (features, fixes, etc.) -3. Be suitable for a git annotated tag - -Format: - Release - -Summary of changes... - -Changes: -- Feature: description -- Fix: description -... -"#; - -const TAG_MESSAGE_SYSTEM_PROMPT_ZH: &str = r#"你是一个生成 git 标签注释消息的助手。 - -给定版本号和提交列表,生成简洁但信息丰富的标签消息。 - -消息应该: -1. 以发布的简要摘要开始 -2. 按类型分组更改(功能、修复等) -3. 适合 git 标注标签 - -格式: - 发布 - -更改摘要... - -更改: -- 功能:描述 -- 修复:描述 -... -"#; - -const TAG_MESSAGE_SYSTEM_PROMPT_JA: &str = r#"あなたはgitタグ注釈メッセージを生成するアシスタントです。 - -バージョン番号とコミットのリストを考慮して、簡潔ですが情報豊富なタグメッセージを生成してください。 - -メッセージは以下のようであるべきです: -1. リリースの簡単な要約から始める -2. タイプ別に変更をグループ化する(機能、修正など) -3. git注釈タグに適している - -形式: - リリース - -変更の概要... - -変更: -- 機能:説明 -- 修正:説明 -... -"#; - -const TAG_MESSAGE_SYSTEM_PROMPT_KO: &str = r#"당신은 git 태그 주석 메시지를 생성하는 도우미입니다. - -버전 번호와 커밋 목록을 고려하여 간결하지만 정보가 풍부한 태그 메시지를 생성하세요. - -메시지는 다음과 같아야 합니다: -1. 릴리스의 간단한 요약으로 시작 -2. 유형별로 변경 사항 그룹화(기능, 수정 등) -3. git 주석 태그에 적합 - -형식: - 릴리스 - -변경 사항 요약... - -변경 사항: -- 기능: 설명 -- 수정: 설명 -... -"#; - -const TAG_MESSAGE_SYSTEM_PROMPT_ES: &str = r#"Eres un asistente que genera mensajes de anotación de etiquetas git. - -Dado un número de versión y una lista de commits, genera un mensaje de etiqueta conciso pero informativo. - -El mensaje debe: -1. Comenzar con un resumen breve de la versión -2. Agrupar cambios por tipo (características, correcciones, etc.) -3. Ser adecuado para una etiqueta git anotada - -Formato: - Versión - -Resumen de cambios... - -Cambios: -- Característica: descripción -- Corrección: descripción -... -"#; - -const TAG_MESSAGE_SYSTEM_PROMPT_FR: &str = r#"Vous êtes un assistant qui génère des messages d'annotation de balises git. - -Étant donné un numéro de version et une liste de commits, générez un message de balise concis mais informatif. - -Le message doit : -1. Commencer par un bref résumé de la version -2. Grouper les changements par type (fonctionnalités, corrections, etc.) -3. Être adapté à une balise git annotée - -Format : - Version - -Résumé des changements... - -Changements : -- Fonctionnalité : description -- Correction : description -... -"#; - -const TAG_MESSAGE_SYSTEM_PROMPT_DE: &str = r#"Sie sind ein Assistent, der git-Tag-Anmerkungsnachrichten generiert. - -Gegeben eine Versionsnummer und eine Liste von Commits, generieren Sie eine prägnante aber informative Tag-Nachricht. - -Die Nachricht sollte: -1. Mit einer kurzen Zusammenfassung der Version beginnen -2. Änderungen nach Typ gruppieren (Funktionen, Fixes, etc.) -3. Für ein git-annotiertes Tag geeignet sein - -Format: - Version - -Zusammenfassung der Änderungen... - -Änderungen: -- Funktion: Beschreibung -- Fix: Beschreibung -... -"#; - -const CHANGELOG_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates changelog entries. - -Given a version and a list of commits, generate a well-formatted changelog section. - -Group commits by: -- Features (feat) -- Bug Fixes (fix) -- Documentation (docs) -- Other Changes - -Format in markdown with proper headings and bullet points. -"#; - -const CHANGELOG_SYSTEM_PROMPT_ZH: &str = r#"你是一个生成变更日志条目的助手。 - -给定版本和提交列表,生成格式良好的变更日志部分。 - -按以下方式分组提交: -- 功能 (feat) -- 错误修复 (fix) -- 文档 (docs) -- 其他更改 - -使用适当的标题和项目符号以 markdown 格式输出。 -"#; - -const CHANGELOG_SYSTEM_PROMPT_JA: &str = r#"あなたは変更ログエントリを生成するアシスタントです。 - -バージョンとコミットのリストを考慮して、適切にフォーマットされた変更ログセクションを生成してください。 - -コミットを以下でグループ化してください: -- 機能 (feat) -- バグ修正 (fix) -- ドキュメント (docs) -- その他の変更 - -適切な見出しと箇条書きを使用してmarkdown形式でフォーマットしてください。 -"#; - -const CHANGELOG_SYSTEM_PROMPT_KO: &str = r#"당신은 변경 로그 항목을 생성하는 도우미입니다. - -버전과 커밋 목록을 고려하여 잘 포맷된 변경 로그 섹션을 생성하세요. - -다음으로 커밋을 그룹화하세요: -- 기능 (feat) -- 버그 수정 (fix) -- 문서 (docs) -- 기타 변경 사항 - -적절한 제목과 글머리 기호를 사용하여 markdown 형식으로 포맷하세요. -"#; - -const CHANGELOG_SYSTEM_PROMPT_ES: &str = r#"Eres un asistente que genera entradas de registro de cambios. - -Dada una versión y una lista de commits, genera una sección de registro de cambios bien formateada. - -Agrupa los commits por: -- Características (feat) -- Correcciones de errores (fix) -- Documentación (docs) -- Otros cambios - -Formatea en markdown con encabezados y viñetas apropiados. -"#; - -const CHANGELOG_SYSTEM_PROMPT_FR: &str = r#"Vous êtes un assistant qui génère des entrées de journal des modifications. - -Étant donné une version et une liste de commits, générez une section de journal des modifications bien formatée. - -Groupez les commits par : -- Fonctionnalités (feat) -- Corrections de bugs (fix) -- Documentation (docs) -- Autres modifications - -Formatez en markdown avec des en-têtes et des puces appropriés. -"#; - -const CHANGELOG_SYSTEM_PROMPT_DE: &str = r#"Sie sind ein Assistent, der Changelog-Einträge generiert. - -Gegeben eine Version und eine Liste von Commits, generieren Sie einen gut formatierten Changelog-Abschnitt. - -Gruppieren Sie Commits nach: -- Funktionen (feat) -- Bugfixes (fix) -- Dokumentation (docs) -- Andere Änderungen - -Formatieren Sie in Markdown mit geeigneten Überschriften und Aufzählungspunkten. -"#; - -/// Test LLM connection -pub async fn test_connection(manager: &crate::config::manager::ConfigManager) -> Result { - let client = LlmClient::from_config(manager).await?; - let response = client.provider.generate("Say 'Hello, World!'").await?; - Ok(response) -} +use crate::config::Language; +use anyhow::{Context, Result, bail}; +use async_trait::async_trait; +use std::time::Duration; + +pub mod anthropic; +pub mod deepseek; +pub mod kimi; +pub mod ollama; +pub mod openai; +pub mod openrouter; +pub mod parsing; +pub mod prompts; +pub mod thinking; + +pub use anthropic::AnthropicClient; +pub use deepseek::DeepSeekClient; +pub use kimi::KimiClient; +pub use ollama::OllamaClient; +pub use openai::OpenAiClient; +pub use openrouter::OpenRouterClient; +pub use parsing::GeneratedCommit; + +/// LLM provider trait +#[async_trait] +pub trait LlmProvider: Send + Sync { + /// Generate text from prompt + async fn generate(&self, prompt: &str) -> Result; + + /// Generate with system prompt + async fn generate_with_system(&self, system: &str, user: &str) -> Result; + + /// Check if provider is available + async fn is_available(&self) -> bool; + + /// Get provider name + fn name(&self) -> &str; +} + +/// LLM client that wraps different providers +pub struct LlmClient { + provider: Box, + config: LlmClientConfig, +} + +#[derive(Debug, Clone)] +pub struct LlmClientConfig { + pub max_tokens: u32, + pub temperature: f32, + pub timeout: Duration, + pub thinking_enabled: bool, +} + +impl Default for LlmClientConfig { + fn default() -> Self { + Self { + max_tokens: 500, + temperature: 0.7, + timeout: Duration::from_secs(30), + thinking_enabled: false, + } + } +} + +impl LlmClient { + /// Create LLM client from configuration manager + pub async fn from_config(manager: &crate::config::manager::ConfigManager) -> Result { + Self::from_config_with_think(manager, manager.config().llm.thinking_enabled).await + } + + /// Create LLM client from configuration with explicit thinking override + pub async fn from_config_with_think( + manager: &crate::config::manager::ConfigManager, + thinking_enabled: bool, + ) -> Result { + let config = manager.config(); + let client_config = LlmClientConfig { + max_tokens: config.llm.max_tokens, + temperature: config.llm.temperature, + timeout: Duration::from_secs(config.llm.timeout), + thinking_enabled, + }; + + let provider = config.llm.provider.as_str(); + let model = config.llm.model.as_str(); + let base_url = manager.llm_base_url(); + let api_key = manager.get_api_key(); + + let provider: Box = match provider { + "ollama" => Box::new( + OllamaClient::new(&base_url, model) + .with_max_tokens(client_config.max_tokens) + .with_temperature(client_config.temperature), + ), + "openai" => { + let key = api_key + .as_ref() + .ok_or_else(|| anyhow::anyhow!("OpenAI API key not configured"))?; + let thinking_state = if thinking_enabled { + Some(thinking::create_console_thinking_state()) + } else { + None + }; + let mut client = OpenAiClient::new(&base_url, key, model)? + .with_thinking(thinking_enabled) + .with_max_tokens(client_config.max_tokens) + .with_temperature(client_config.temperature) + .with_timeout(client_config.timeout)?; + if let Some(state) = thinking_state { + client = client.with_thinking_state(state); + } + Box::new(client) + } + "anthropic" => { + let key = api_key + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Anthropic API key not configured"))?; + let thinking_state = if thinking_enabled { + Some(thinking::create_console_thinking_state()) + } else { + None + }; + let budget = config.llm.thinking_budget_tokens.unwrap_or(1024); + let mut client = AnthropicClient::new(key, model)? + .with_thinking(thinking_enabled) + .with_thinking_budget_tokens(budget) + .with_max_tokens(client_config.max_tokens) + .with_temperature(client_config.temperature) + .with_timeout(client_config.timeout)?; + if let Some(state) = thinking_state { + client = client.with_thinking_state(state); + } + Box::new(client) + } + "kimi" => { + let key = api_key + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Kimi API key not configured"))?; + let thinking_state = if thinking_enabled { + Some(thinking::create_console_thinking_state()) + } else { + None + }; + let mut client = KimiClient::with_base_url(key, model, &base_url)? + .with_thinking(thinking_enabled) + .with_max_tokens(client_config.max_tokens) + .with_temperature(client_config.temperature) + .with_timeout(client_config.timeout)?; + if let Some(state) = thinking_state { + client = client.with_thinking_state(state); + } + Box::new(client) + } + "deepseek" => { + let key = api_key + .as_ref() + .ok_or_else(|| anyhow::anyhow!("DeepSeek API key not configured"))?; + let thinking_state = if thinking_enabled { + Some(thinking::create_console_thinking_state()) + } else { + None + }; + let mut client = DeepSeekClient::with_base_url(key, model, &base_url)? + .with_thinking(thinking_enabled) + .with_max_tokens(client_config.max_tokens) + .with_temperature(client_config.temperature) + .with_timeout(client_config.timeout)?; + if let Some(state) = thinking_state { + client = client.with_thinking_state(state); + } + Box::new(client) + } + "openrouter" => { + let key = api_key + .as_ref() + .ok_or_else(|| anyhow::anyhow!("OpenRouter API key not configured"))?; + Box::new( + OpenRouterClient::with_base_url(key, model, &base_url)? + .with_max_tokens(client_config.max_tokens) + .with_temperature(client_config.temperature) + .with_timeout(client_config.timeout)?, + ) + } + _ => bail!("Unknown LLM provider: {}", provider), + }; + + Ok(Self { + provider, + config: client_config, + }) + } + + /// Create with specific provider + pub fn with_provider(provider: Box) -> Self { + Self { + provider, + config: LlmClientConfig::default(), + } + } + + /// Generate commit message from git diff + pub async fn generate_commit_message( + &self, + diff: &str, + format: crate::config::CommitFormat, + language: Language, + template: Option<&str>, + ) -> Result { + let mut system_prompt = prompts::get_commit_system_prompt(format, language).to_string(); + + if let Some(tmpl) = template { + system_prompt.push_str(&format!( + "\n\n## Commit Message Template\nFollow this template structure:\n{}", + tmpl + )); + } + + // Add language instruction to the prompt + let language_instruction = match language { + Language::Chinese => "\n\n请用中文生成提交消息。", + Language::Japanese => "\n\n日本語でコミットメッセージを生成してください。", + Language::Korean => "\n\n한국어로 커밋 메시지를 생성하세요.", + Language::Spanish => "\n\nPor favor, genera el mensaje de commit en español.", + Language::French => "\n\nVeuillez générer le message de commit en français.", + Language::German => "\n\nBitte generieren Sie die Commit-Nachricht auf Deutsch.", + Language::English => "", + }; + + let prompt = format!("{}{}", diff, language_instruction); + let response = self + .provider + .generate_with_system(&system_prompt, &prompt) + .await?; + + parsing::parse_commit_response(&response, format) + } + + /// Generate tag message from commits + pub async fn generate_tag_message( + &self, + version: &str, + commits: &[String], + language: Language, + ) -> Result { + let system_prompt = prompts::get_tag_system_prompt(language); + let commits_text = commits.join("\n"); + + // Add language instruction to the prompt + let language_instruction = match language { + Language::Chinese => "\n\n请用中文生成标签消息。", + Language::Japanese => "\n\n日本語でタグメッセージを生成してください。", + Language::Korean => "\n\n한국어로 태그 메시지를 생성하세요.", + Language::Spanish => "\n\nPor favor, genera el mensaje de etiqueta en español.", + Language::French => "\n\nVeuillez générer le message de balise en français.", + Language::German => "\n\nBitte generieren Sie die Tag-Nachricht auf Deutsch.", + Language::English => "", + }; + + let prompt = format!( + "Version: {}\n\nCommits:\n{}{}", + version, commits_text, language_instruction + ); + + self.provider + .generate_with_system(system_prompt, &prompt) + .await + } + + /// Generate changelog entry + pub async fn generate_changelog_entry( + &self, + version: &str, + commits: &[(String, String)], // (type, message) + language: Language, + ) -> Result { + let system_prompt = prompts::get_changelog_system_prompt(language); + + let commits_text = commits + .iter() + .map(|(t, m)| format!("- [{}] {}", t, m)) + .collect::>() + .join("\n"); + + // Add language instruction to the prompt + let language_instruction = match language { + Language::Chinese => "\n\n请用中文生成变更日志。", + Language::Japanese => "\n\n日本語で変更ログを生成してください。", + Language::Korean => "\n\n한국어로 변경 로그를 생성하세요.", + Language::Spanish => "\n\nPor favor, genera el registro de cambios en español.", + Language::French => "\n\nVeuillez générer le journal des modifications en français.", + Language::German => "\n\nBitte generieren Sie das Changelog auf Deutsch.", + Language::English => "", + }; + + let prompt = format!( + "Version: {}\n\nCommits:\n{}{}", + version, commits_text, language_instruction + ); + + self.provider + .generate_with_system(system_prompt, &prompt) + .await + } + + /// Check if provider is available + pub async fn is_available(&self) -> bool { + self.provider.is_available().await + } + +} + + +/// HTTP client helper +pub(crate) fn create_http_client(timeout: Duration) -> Result { + reqwest::Client::builder() + .timeout(timeout) + .build() + .context("Failed to create HTTP client") +} + + +/// Test LLM connection +pub async fn test_connection(manager: &crate::config::manager::ConfigManager) -> Result { + let client = LlmClient::from_config(manager).await?; + let response = client.provider.generate("Say 'Hello, World!'").await?; + Ok(response) +} \ No newline at end of file diff --git a/src/llm/parsing.rs b/src/llm/parsing.rs new file mode 100644 index 0000000..926ea43 --- /dev/null +++ b/src/llm/parsing.rs @@ -0,0 +1,281 @@ +use anyhow::{Result, bail}; + +/// Parse commit response from LLM +pub(crate) fn parse_commit_response( + response: &str, + format: crate::config::CommitFormat, +) -> Result { + // Clean markdown code fences from the response + let cleaned = strip_code_fences(response); + + let lines: Vec<&str> = cleaned + .lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty()) + .collect(); + + if lines.is_empty() { + let preview: String = response.chars().take(200).collect(); + bail!( + "LLM returned empty or whitespace-only response. \ + Raw response preview: '{}'. \ + Hint: If using DeepSeek/Kimi with thinking enabled, \ + the model may have returned reasoning_content only. \ + Try disabling thinking mode or switching models.", + preview + ); + } + + // Find the line most likely to be the commit subject + let first_line = find_commit_subject_line(&lines, format); + + // Parse based on format + match format { + crate::config::CommitFormat::Conventional => { + parse_conventional_commit(first_line, &lines, response) + } + crate::config::CommitFormat::Commitlint => { + parse_commitlint_commit(first_line, &lines, response) + } + } +} + +/// Remove surrounding markdown code fences (```) from LLM output +fn strip_code_fences(response: &str) -> String { + let mut lines: Vec<&str> = response.lines().collect(); + + // Strip leading fence lines (``` or ```lang) + while lines.first().map_or(false, |l| l.trim().starts_with("```")) { + lines.remove(0); + } + + // Strip trailing fence lines + while lines.last().map_or(false, |l| l.trim() == "```") { + lines.pop(); + } + + lines.join("\n") +} + +/// Find the line that is most likely the commit subject among extracted lines +fn find_commit_subject_line<'a>( + lines: &[&'a str], + format: crate::config::CommitFormat, +) -> &'a str { + let valid_types = crate::utils::validators::get_commit_types(matches!( + format, + crate::config::CommitFormat::Commitlint + )); + + // First pass: line starting with a known type that also has proper syntax + // (e.g. "type:", "type(scope):", "type!:") + for &line in lines { + let trimmed = line.trim(); + for &t in valid_types { + if let Some(rest) = trimmed.strip_prefix(t) { + if rest.starts_with(':') || rest.starts_with('(') || rest.starts_with("!:") { + return trimmed; + } + } + } + } + + // Second pass: any line containing a colon (generic "prefix: description") + for &line in lines { + if line.contains(':') { + return line.trim(); + } + } + + // Fallback: return the first line as-is + lines[0].trim() +} + +fn parse_conventional_commit( + first_line: &str, + lines: &[&str], + raw_response: &str, +) -> Result { + // Parse: type(scope)!: description + let parts: Vec<&str> = first_line.splitn(2, ':').collect(); + if parts.len() != 2 { + let preview: String = raw_response.chars().take(300).collect(); + bail!( + "Invalid conventional commit format: missing colon.\n\ + Parsed subject line: '{}'\n\ + Raw response preview: '{}'\n\ + Expected: [optional scope]: ", + first_line, + preview + ); + } + + let type_part = parts[0]; + let description = parts[1].trim(); + + // Extract type, scope, and breaking indicator + let breaking = type_part.ends_with('!'); + let type_part = type_part.trim_end_matches('!'); + + let (commit_type, scope) = if let Some(start) = type_part.find('(') { + if let Some(end) = type_part.find(')') { + let t = &type_part[..start]; + let s = &type_part[start + 1..end]; + (t.to_string(), Some(s.to_string())) + } else { + bail!("Invalid scope format: missing closing parenthesis"); + } + } else { + (type_part.to_string(), None) + }; + + // Extract body and footer + let (body, footer) = extract_body_footer(lines); + + Ok(GeneratedCommit { + commit_type, + scope, + description: description.to_string(), + body, + footer, + breaking, + }) +} + +fn parse_commitlint_commit( + first_line: &str, + lines: &[&str], + raw_response: &str, +) -> Result { + // Similar parsing but with commitlint rules + let parts: Vec<&str> = first_line.splitn(2, ':').collect(); + if parts.len() != 2 { + let preview: String = raw_response.chars().take(300).collect(); + bail!( + "Invalid commit format: missing colon.\n\ + Parsed subject line: '{}'\n\ + Raw response preview: '{}'\n\ + Expected: [optional scope]: ", + first_line, + preview + ); + } + + let type_part = parts[0]; + let subject = parts[1].trim(); + + let (commit_type, scope) = if let Some(start) = type_part.find('(') { + if let Some(end) = type_part.find(')') { + let t = &type_part[..start]; + let s = &type_part[start + 1..end]; + (t.to_string(), Some(s.to_string())) + } else { + (type_part.to_string(), None) + } + } else { + (type_part.to_string(), None) + }; + + let (body, footer) = extract_body_footer(&lines); + + Ok(GeneratedCommit { + commit_type, + scope, + description: subject.to_string(), + body, + footer, + breaking: false, + }) +} + +fn extract_body_footer(lines: &[&str]) -> (Option, Option) { + if lines.len() <= 1 { + return (None, None); + } + + let rest: Vec<&str> = lines[1..] + .iter() + .skip_while(|l| l.trim().is_empty()) + .copied() + .collect(); + + if rest.is_empty() { + return (None, None); + } + + // Look for footer markers + let footer_markers = [ + "BREAKING CHANGE:", + "Closes", + "Fixes", + "Refs", + "Co-authored-by:", + ]; + + let mut body_lines = vec![]; + let mut footer_lines = vec![]; + let mut in_footer = false; + + for line in &rest { + if footer_markers.iter().any(|m| line.starts_with(m)) { + in_footer = true; + } + + if in_footer { + footer_lines.push(*line); + } else { + body_lines.push(*line); + } + } + + let body = if body_lines.is_empty() { + None + } else { + Some(body_lines.join("\n")) + }; + + let footer = if footer_lines.is_empty() { + None + } else { + Some(footer_lines.join("\n")) + }; + + (body, footer) +} + +/// Generated commit structure +#[derive(Debug, Clone)] +pub struct GeneratedCommit { + pub commit_type: String, + pub scope: Option, + pub description: String, + pub body: Option, + pub footer: Option, + pub breaking: bool, +} + +impl GeneratedCommit { + /// Format as conventional commit + pub fn to_conventional(&self) -> String { + crate::utils::formatter::format_conventional_commit( + &self.commit_type, + self.scope.as_deref(), + &self.description, + self.body.as_deref(), + self.footer.as_deref(), + self.breaking, + ) + } + + /// Format as commitlint commit + pub fn to_commitlint(&self) -> String { + crate::utils::formatter::format_commitlint_commit( + &self.commit_type, + self.scope.as_deref(), + &self.description, + self.body.as_deref(), + self.footer.as_deref(), + None, + ) + } +} diff --git a/src/llm/prompts.rs b/src/llm/prompts.rs new file mode 100644 index 0000000..401b40f --- /dev/null +++ b/src/llm/prompts.rs @@ -0,0 +1,618 @@ +use crate::config::Language; + +/// Get commit system prompt based on format and language +pub(crate) fn get_commit_system_prompt( + format: crate::config::CommitFormat, + language: Language, +) -> &'static str { + match (format, language) { + (crate::config::CommitFormat::Conventional, Language::Chinese) => { + CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ZH + } + (crate::config::CommitFormat::Conventional, Language::Japanese) => { + CONVENTIONAL_COMMIT_SYSTEM_PROMPT_JA + } + (crate::config::CommitFormat::Conventional, Language::Korean) => { + CONVENTIONAL_COMMIT_SYSTEM_PROMPT_KO + } + (crate::config::CommitFormat::Conventional, Language::Spanish) => { + CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ES + } + (crate::config::CommitFormat::Conventional, Language::French) => { + CONVENTIONAL_COMMIT_SYSTEM_PROMPT_FR + } + (crate::config::CommitFormat::Conventional, Language::German) => { + CONVENTIONAL_COMMIT_SYSTEM_PROMPT_DE + } + (crate::config::CommitFormat::Conventional, _) => CONVENTIONAL_COMMIT_SYSTEM_PROMPT, + (crate::config::CommitFormat::Commitlint, Language::Chinese) => COMMITLINT_SYSTEM_PROMPT_ZH, + (crate::config::CommitFormat::Commitlint, Language::Japanese) => { + COMMITLINT_SYSTEM_PROMPT_JA + } + (crate::config::CommitFormat::Commitlint, Language::Korean) => COMMITLINT_SYSTEM_PROMPT_KO, + (crate::config::CommitFormat::Commitlint, Language::Spanish) => COMMITLINT_SYSTEM_PROMPT_ES, + (crate::config::CommitFormat::Commitlint, Language::French) => COMMITLINT_SYSTEM_PROMPT_FR, + (crate::config::CommitFormat::Commitlint, Language::German) => COMMITLINT_SYSTEM_PROMPT_DE, + (crate::config::CommitFormat::Commitlint, _) => COMMITLINT_SYSTEM_PROMPT, + } +} + +pub(crate) fn get_tag_system_prompt(language: Language) -> &'static str { + match language { + Language::Chinese => TAG_MESSAGE_SYSTEM_PROMPT_ZH, + Language::Japanese => TAG_MESSAGE_SYSTEM_PROMPT_JA, + Language::Korean => TAG_MESSAGE_SYSTEM_PROMPT_KO, + Language::Spanish => TAG_MESSAGE_SYSTEM_PROMPT_ES, + Language::French => TAG_MESSAGE_SYSTEM_PROMPT_FR, + Language::German => TAG_MESSAGE_SYSTEM_PROMPT_DE, + _ => TAG_MESSAGE_SYSTEM_PROMPT, + } +} + +pub(crate) fn get_changelog_system_prompt(language: Language) -> &'static str { + match language { + Language::Chinese => CHANGELOG_SYSTEM_PROMPT_ZH, + Language::Japanese => CHANGELOG_SYSTEM_PROMPT_JA, + Language::Korean => CHANGELOG_SYSTEM_PROMPT_KO, + Language::Spanish => CHANGELOG_SYSTEM_PROMPT_ES, + Language::French => CHANGELOG_SYSTEM_PROMPT_FR, + Language::German => CHANGELOG_SYSTEM_PROMPT_DE, + _ => CHANGELOG_SYSTEM_PROMPT, + } +} + +// System prompts for LLM + +const CONVENTIONAL_COMMIT_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates conventional commit messages. + +Analyze the git diff provided and generate a commit message following the Conventional Commits specification. + +Format: [optional scope]: + +Types: +- feat: A new feature +- fix: A bug fix +- docs: Documentation only changes +- style: Changes that don't affect code meaning (formatting, semicolons, etc.) +- refactor: Code change that neither fixes a bug nor adds a feature +- perf: Code change that improves performance +- test: Adding or correcting tests +- build: Changes to build system or dependencies +- ci: Changes to CI configuration +- chore: Other changes that don't modify src or test files +- revert: Reverts a previous commit + +Rules: +1. Use lowercase for type and scope +2. Keep description under 100 characters +3. Use imperative mood ("add" not "added") +4. Don't capitalize first letter +5. No period at the end +6. Include scope if the change is specific to a module/component + +Output ONLY the commit message, nothing else. +"#; + +const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ZH: &str = r#"你是一个生成符合 Conventional Commits 规范的提交消息的助手。 + +分析提供的 git diff,并生成符合 Conventional Commits 规范的提交消息。 + +格式: [可选作用域]: <描述> + +类型: +- feat: 新功能 +- fix: 修复错误 +- docs: 仅文档更改 +- style: 不影响代码含义的更改(格式化、分号等) +- refactor: 既不修复错误也不添加功能的代码更改 +- perf: 提高性能的代码更改 +- test: 添加或更正测试 +- build: 更改构建系统或依赖项 +- ci: 更改 CI 配置 +- chore: 其他不修改 src 或测试文件的更改 +- revert: 撤销之前的提交 + +规则: +1. 类型和小写使用小写 +2. 描述保持在 100 个字符以内 +3. 使用祈使语气("添加"而不是"已添加") +4. 不要大写首字母 +5. 结尾不要句号 +6. 如果更改特定于模块/组件,请包含作用域 +7. 仅输出提交消息,不要输出其他内容。 +"#; + +const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_JA: &str = r#"あなたはConventional Commits仕様に従ったコミットメッセージを生成するアシスタントです。 + +提供されたgit diffを分析し、Conventional Commits仕様に従ったコミットメッセージを生成してください。 + +形式: [オプションのスコープ]: <説明> + +タイプ: +- feat: 新機能 +- fix: バグ修正 +- docs: ドキュメントのみの変更 +- style: コードの意味に影響しない変更(フォーマット、セミコロンなど) +- refactor: バグ修正や機能追加を伴わないコード変更 +- perf: パフォーマンスを向上させるコード変更 +- test: テストの追加または修正 +- build: ビルドシステムまたは依存関係の変更 +- ci: CI設定の変更 +- chore: srcやテストファイルを変更しないその他の変更 +- revert: 以前のコミットを取り消す + +ルール: +1. タイプとスコープは小文字を使用 +2. 説明は100文字以内にする +3. 命令形を使用する("追加"ではなく"追加する") +4. 先頭を大文字にしない +5. 最後にピリオドを付けない +6. 変更がモジュール/コンポーネントに固有の場合はスコープを含める +7. コミットメッセージのみを出力し、それ以外は出力しないでください。 +"#; + +const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_KO: &str = r#"당신은 Conventional Commits 사양에 따른 커밋 메시지를 생성하는 도우미입니다. + +제공된 git diff를 분석하고 Conventional Commits 사양에 따른 커밋 메시지를 생성하세요. + +형식: [선택적 범위]: <설명> + +유형: +- feat: 새 기능 +- fix: 버그 수정 +- docs: 문서 변경만 +- style: 코드 의미에 영향을 주지 않는 변경(서식, 세미콜론 등) +- refactor: 버그를 수정하거나 기능을 추가하지 않는 코드 변경 +- perf: 성능을 향상시키는 코드 변경 +- test: 테스트 추가 또는 수정 +- build: 빌드 시스템 또는 종속성 변경 +- ci: CI 구성 변경 +- chore: src 또는 테스트 파일을 수정하지 않는 기타 변경 +- revert: 이전 커밋 되돌리기 + +규칙: +1. 유형과 범위는 소문자 사용 +2. 설명은 100자 이내로 유지 +3. 명령형 사용("추가"가 아닌 "추가하다") +4. 첫 글자 대문자화하지 않음 +5. 끝에 마침표 사용하지 않음 +6. 변경 사항이 모듈/구성 요소에 특정한 경우 범위 포함 +7. 커밋 메시지만 출력하고 다른 내용은 출력하지 마세요. +"#; + +const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_ES: &str = r#"Eres un asistente que genera mensajes de commit siguiendo la especificación Conventional Commits. + +Analiza el diff de git proporcionado y genera un mensaje de commit siguiendo la especificación Conventional Commits. + +Formato: [alcance opcional]: + +Tipos: +- feat: Una nueva característica +- fix: Una corrección de error +- docs: Solo cambios en documentación +- style: Cambios que no afectan el significado del código (formato, punto y coma, etc.) +- refactor: Cambio de código que no corrige un error ni agrega una característica +- perf: Cambio de código que mejora el rendimiento +- test: Agregar o corregir pruebas +- build: Cambios en el sistema de construcción o dependencias +- ci: Cambios en la configuración de CI +- chore: Otros cambios que no modifican archivos src o de prueba +- revert: Revierte un commit anterior + +Reglas: +1. Usa minúsculas para tipo y alcance +2. Mantén la descripción bajo 100 caracteres +3. Usa modo imperativo ("agregar" no "agregado") +4. No capitalices la primera letra +5. Sin punto al final +6. Incluye alcance si el cambio es específico de un módulo/componente +7. Genera SOLO el mensaje de commit, nada más. +"#; + +const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_FR: &str = r#"Vous êtes un assistant qui génère des messages de commit suivant la spécification Conventional Commits. + +Analysez le diff git fourni et générez un message de commit suivant la spécification Conventional Commits. + +Format: [portée optionnelle]: + +Types: +- feat: Une nouvelle fonctionnalité +- fix: Une correction de bug +- docs: Changements de documentation uniquement +- style: Changements qui n'affectent pas la signification du code (formatage, points-virgules, etc.) +- refactor: Changement de code qui ne corrige pas un bug ni n'ajoute une fonctionnalité +- perf: Changement de code qui améliore les performances +- test: Ajout ou correction de tests +- build: Changements du système de build ou des dépendances +- ci: Changements de la configuration CI +- chore: Autres changements qui ne modifient pas les fichiers src ou de test +- revert: Révertit un commit précédent + +Règles: +1. Utilisez des minuscules pour le type et la portée +2. Gardez la description sous 100 caractères +3. Utilisez le mode impératif ("ajouter" non "ajouté") +4. Ne capitalisez pas la première lettre +5. Pas de point à la fin +6. Incluez la portée si le changement est spécifique à un module/composant +7. Générez SEULEMENT le message de commit, rien d'autre. +"#; + +const CONVENTIONAL_COMMIT_SYSTEM_PROMPT_DE: &str = r#"Sie sind ein Assistent, der Commit-Nachrichten gemäß der Conventional Commits-Spezifikation generiert. + +Analysieren Sie den bereitgestellten git diff und generieren Sie eine Commit-Nachricht gemäß der Conventional Commits-Spezifikation. + +Format: [optionaler Bereich]: + +Typen: +- feat: Eine neue Funktion +- fix: Ein Bugfix +- docs: Nur Dokumentationsänderungen +- style: Änderungen, die die Code-Bedeutung nicht beeinflussen (Formatierung, Semikolons usw.) +- refactor: Code-Änderung, die weder einen Bug behebt noch eine Funktion hinzufügt +- perf: Code-Änderung, die die Leistung verbessert +- test: Hinzufügen oder Korrigieren von Tests +- build: Änderungen am Build-System oder Abhängigkeiten +- ci: Änderungen an der CI-Konfiguration +- chore: Andere Änderungen, die src- oder Testdateien nicht ändern +- revert: Setzt einen vorherigen Commit zurück + +Regeln: +1. Verwenden Sie Kleinbuchstaben für Typ und Bereich +2. Halten Sie die Beschreibung unter 100 Zeichen +3. Verwenden Sie den Imperativ ("hinzufügen" nicht "hinzugefügt") +4. Großschreiben Sie den ersten Buchstaben nicht +5. Kein Punkt am Ende +6. Fügen Sie einen Bereich ein, wenn die Änderung spezifisch für ein Modul/Komponente ist +7. Geben Sie NUR die Commit-Nachricht aus, nichts anderes. +"#; + +const COMMITLINT_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates commit messages following @commitlint/config-conventional. + +Analyze the git diff and generate a commit message. + +Format: [optional scope]: + +Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + +Rules: +1. Subject should not start with uppercase +2. Subject should not end with period +3. Subject should be 4-100 characters +4. Use imperative mood +5. Be concise but descriptive +6. Output ONLY the commit message, nothing else. +"#; + +const COMMITLINT_SYSTEM_PROMPT_ZH: &str = r#"你是一个生成符合 @commitlint/config-conventional 规范的提交消息的助手。 + +分析 git diff 并生成提交消息。 + +格式: [可选作用域]: <主题> + +类型: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + +规则: +1. 主题不应以大写字母开头 +2. 主题不应以句号结尾 +3. 主题应为 4-100 个字符 +4. 使用祈使语气 +5. 简洁但描述性强 +6. 仅输出提交消息,不要输出其他额外内容。 +"#; + +const COMMITLINT_SYSTEM_PROMPT_JA: &str = r#"あなたは@commitlint/config-conventionalに従ったコミットメッセージを生成するアシスタントです。 + +git diffを分析し、コミットメッセージを生成してください。 + +形式: [オプションのスコープ]: <件名> + +タイプ: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + +ルール: +1. 件名は大文字で始めないでください +2. 件名はピリオドで終わらないでください +3. 件名は4-100文字である必要があります +4. 命令形を使用してください +5. 簡潔ですが説明的であること +6. コミットメッセージのみを出力し、それ以外は出力しないでください。 +"#; + +const COMMITLINT_SYSTEM_PROMPT_KO: &str = r#"당신은 @commitlint/config-conventional에 따른 커밋 메시지를 생성하는 도우미입니다. + +git diff를 분석하고 커밋 메시지를 생성하세요. + +형식: [선택적 범위]: <제목> + +유형: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + +규칙: +1. 제목은 대문자로 시작하지 않아야 합니다 +2. 제목은 마침표로 끝나지 않아야 합니다 +3. 제목은 4-100자여야 합니다 +4. 명령형을 사용하세요 +5. 간결하지만 설명적이어야 합니다 +6. 커밋 메시지만 출력하고 다른 내용은 출력하지 마세요. +"#; + +const COMMITLINT_SYSTEM_PROMPT_ES: &str = r#"Eres un asistente que genera mensajes de commit siguiendo @commitlint/config-conventional. + +Analiza el diff de git y genera un mensaje de commit. + +Formato: [alcance opcional]: + +Tipos: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + +Reglas: +1. El asunto no debe comenzar con mayúscula +2. El asunto no debe terminar con punto +3. El asunto debe tener 4-100 caracteres +4. Usa modo imperativo +5. Sé conciso pero descriptivo +6. Genera SOLO el mensaje de commit, nada más. +"#; + +const COMMITLINT_SYSTEM_PROMPT_FR: &str = r#"Vous êtes un assistant qui génère des messages de commit suivant @commitlint/config-conventional. + +Analysez le diff git et générez un message de commit. + +Format: [portée optionnelle]: + +Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + +Règles: +1. Le sujet ne doit pas commencer par une majuscule +2. Le sujet ne doit pas se terminer par un point +3. Le sujet doit avoir 4-100 caractères +4. Utilisez le mode impératif +5. Soyez concis mais descriptif +6. Générez SEULEMENT le message de commit, rien d'autre. +"#; + +const COMMITLINT_SYSTEM_PROMPT_DE: &str = r#"Sie sind ein Assistent, der Commit-Nachrichten gemäß @commitlint/config-conventional generiert. + +Analysieren Sie den git diff und generieren Sie eine Commit-Nachricht. + +Format: [optionaler Bereich]: + +Typen: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert + +Regeln: +1. Der Betreff sollte nicht mit einem Großbuchstaben beginnen +2. Der Betreff sollte nicht mit einem Punkt enden +3. Der Betreff sollte 4-100 Zeichen haben +4. Verwenden Sie den Imperativ +5. Seien Sie prägnant aber beschreibend +6. Geben Sie NUR die Commit-Nachricht aus, nichts anderes. +"#; + +const TAG_MESSAGE_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates git tag annotation messages. + +Given a version number and a list of commits, generate a concise but informative tag message. + +The message should: +1. Start with a brief summary of the release +2. Group changes by type (features, fixes, etc.) +3. Be suitable for a git annotated tag + +Format: + Release + +Summary of changes... + +Changes: +- Feature: description +- Fix: description +... +"#; + +const TAG_MESSAGE_SYSTEM_PROMPT_ZH: &str = r#"你是一个生成 git 标签注释消息的助手。 + +给定版本号和提交列表,生成简洁但信息丰富的标签消息。 + +消息应该: +1. 以发布的简要摘要开始 +2. 按类型分组更改(功能、修复等) +3. 适合 git 标注标签 + +格式: + 发布 + +更改摘要... + +更改: +- 功能:描述 +- 修复:描述 +... +"#; + +const TAG_MESSAGE_SYSTEM_PROMPT_JA: &str = r#"あなたはgitタグ注釈メッセージを生成するアシスタントです。 + +バージョン番号とコミットのリストを考慮して、簡潔ですが情報豊富なタグメッセージを生成してください。 + +メッセージは以下のようであるべきです: +1. リリースの簡単な要約から始める +2. タイプ別に変更をグループ化する(機能、修正など) +3. git注釈タグに適している + +形式: + リリース + +変更の概要... + +変更: +- 機能:説明 +- 修正:説明 +... +"#; + +const TAG_MESSAGE_SYSTEM_PROMPT_KO: &str = r#"당신은 git 태그 주석 메시지를 생성하는 도우미입니다. + +버전 번호와 커밋 목록을 고려하여 간결하지만 정보가 풍부한 태그 메시지를 생성하세요. + +메시지는 다음과 같아야 합니다: +1. 릴리스의 간단한 요약으로 시작 +2. 유형별로 변경 사항 그룹화(기능, 수정 등) +3. git 주석 태그에 적합 + +형식: + 릴리스 + +변경 사항 요약... + +변경 사항: +- 기능: 설명 +- 수정: 설명 +... +"#; + +const TAG_MESSAGE_SYSTEM_PROMPT_ES: &str = r#"Eres un asistente que genera mensajes de anotación de etiquetas git. + +Dado un número de versión y una lista de commits, genera un mensaje de etiqueta conciso pero informativo. + +El mensaje debe: +1. Comenzar con un resumen breve de la versión +2. Agrupar cambios por tipo (características, correcciones, etc.) +3. Ser adecuado para una etiqueta git anotada + +Formato: + Versión + +Resumen de cambios... + +Cambios: +- Característica: descripción +- Corrección: descripción +... +"#; + +const TAG_MESSAGE_SYSTEM_PROMPT_FR: &str = r#"Vous êtes un assistant qui génère des messages d'annotation de balises git. + +Étant donné un numéro de version et une liste de commits, générez un message de balise concis mais informatif. + +Le message doit : +1. Commencer par un bref résumé de la version +2. Grouper les changements par type (fonctionnalités, corrections, etc.) +3. Être adapté à une balise git annotée + +Format : + Version + +Résumé des changements... + +Changements : +- Fonctionnalité : description +- Correction : description +... +"#; + +const TAG_MESSAGE_SYSTEM_PROMPT_DE: &str = r#"Sie sind ein Assistent, der git-Tag-Anmerkungsnachrichten generiert. + +Gegeben eine Versionsnummer und eine Liste von Commits, generieren Sie eine prägnante aber informative Tag-Nachricht. + +Die Nachricht sollte: +1. Mit einer kurzen Zusammenfassung der Version beginnen +2. Änderungen nach Typ gruppieren (Funktionen, Fixes, etc.) +3. Für ein git-annotiertes Tag geeignet sein + +Format: + Version + +Zusammenfassung der Änderungen... + +Änderungen: +- Funktion: Beschreibung +- Fix: Beschreibung +... +"#; + +const CHANGELOG_SYSTEM_PROMPT: &str = r#"You are a helpful assistant that generates changelog entries. + +Given a version and a list of commits, generate a well-formatted changelog section. + +Group commits by: +- Features (feat) +- Bug Fixes (fix) +- Documentation (docs) +- Other Changes + +Format in markdown with proper headings and bullet points. +"#; + +const CHANGELOG_SYSTEM_PROMPT_ZH: &str = r#"你是一个生成变更日志条目的助手。 + +给定版本和提交列表,生成格式良好的变更日志部分。 + +按以下方式分组提交: +- 功能 (feat) +- 错误修复 (fix) +- 文档 (docs) +- 其他更改 + +使用适当的标题和项目符号以 markdown 格式输出。 +"#; + +const CHANGELOG_SYSTEM_PROMPT_JA: &str = r#"あなたは変更ログエントリを生成するアシスタントです。 + +バージョンとコミットのリストを考慮して、適切にフォーマットされた変更ログセクションを生成してください。 + +コミットを以下でグループ化してください: +- 機能 (feat) +- バグ修正 (fix) +- ドキュメント (docs) +- その他の変更 + +適切な見出しと箇条書きを使用してmarkdown形式でフォーマットしてください。 +"#; + +const CHANGELOG_SYSTEM_PROMPT_KO: &str = r#"당신은 변경 로그 항목을 생성하는 도우미입니다. + +버전과 커밋 목록을 고려하여 잘 포맷된 변경 로그 섹션을 생성하세요. + +다음으로 커밋을 그룹화하세요: +- 기능 (feat) +- 버그 수정 (fix) +- 문서 (docs) +- 기타 변경 사항 + +적절한 제목과 글머리 기호를 사용하여 markdown 형식으로 포맷하세요. +"#; + +const CHANGELOG_SYSTEM_PROMPT_ES: &str = r#"Eres un asistente que genera entradas de registro de cambios. + +Dada una versión y una lista de commits, genera una sección de registro de cambios bien formateada. + +Agrupa los commits por: +- Características (feat) +- Correcciones de errores (fix) +- Documentación (docs) +- Otros cambios + +Formatea en markdown con encabezados y viñetas apropiados. +"#; + +const CHANGELOG_SYSTEM_PROMPT_FR: &str = r#"Vous êtes un assistant qui génère des entrées de journal des modifications. + +Étant donné une version et une liste de commits, générez une section de journal des modifications bien formatée. + +Groupez les commits par : +- Fonctionnalités (feat) +- Corrections de bugs (fix) +- Documentation (docs) +- Autres modifications + +Formatez en markdown avec des en-têtes et des puces appropriés. +"#; + +const CHANGELOG_SYSTEM_PROMPT_DE: &str = r#"Sie sind ein Assistent, der Changelog-Einträge generiert. + +Gegeben eine Version und eine Liste von Commits, generieren Sie einen gut formatierten Changelog-Abschnitt. + +Gruppieren Sie Commits nach: +- Funktionen (feat) +- Bugfixes (fix) +- Dokumentation (docs) +- Andere Änderungen + +Formatieren Sie in Markdown mit geeigneten Überschriften und Aufzählungspunkten. +"#;