diff --git a/app/local-ai-commit-reviewer/src/config/config.py b/app/local-ai-commit-reviewer/src/config/config.py new file mode 100644 index 0000000..71a62b5 --- /dev/null +++ b/app/local-ai-commit-reviewer/src/config/config.py @@ -0,0 +1,164 @@ +import os +from pathlib import Path +from typing import Any + +import yaml # type: ignore[import-untyped] +from pydantic import BaseModel, Field + + +class LLMConfig(BaseModel): + endpoint: str = "http://localhost:11434" + model: str = "codellama" + timeout: int = 120 + max_tokens: int = 2048 + temperature: float = 0.3 + + +class ReviewSettings(BaseModel): + strictness: str = "balanced" + max_issues_per_file: int = 20 + syntax_highlighting: bool = True + show_line_numbers: bool = True + + +class LanguageConfig(BaseModel): + enabled: bool = True + review_rules: list[str] = Field(default_factory=list) + max_line_length: int = 100 + + +class Languages(BaseModel): + python: LanguageConfig = Field(default_factory=lambda: LanguageConfig(review_rules=["pep8", "type-hints", "docstrings"])) + javascript: LanguageConfig = Field(default_factory=lambda: LanguageConfig(review_rules=["airbnb"])) + typescript: LanguageConfig = Field(default_factory=lambda: LanguageConfig(review_rules=["airbnb"])) + go: LanguageConfig = Field(default_factory=lambda: LanguageConfig(review_rules=["golint", "staticcheck"])) + rust: LanguageConfig = Field(default_factory=lambda: LanguageConfig(review_rules=["clippy"])) + java: LanguageConfig = Field(default_factory=lambda: LanguageConfig(review_rules=["google-java"])) + c: LanguageConfig = Field(default_factory=lambda: LanguageConfig(review_rules=["cppcheck"])) + cpp: LanguageConfig = Field(default_factory=lambda: LanguageConfig(review_rules=["cppcheck"])) + + def get_language_config(self, language: str) -> LanguageConfig | None: + return getattr(self, language.lower(), None) + + +class StrictnessProfile(BaseModel): + description: str = "" + check_security: bool = True + check_bugs: bool = True + check_style: bool = True + check_performance: bool = False + check_documentation: bool = False + min_severity: str = "info" + + +class StrictnessProfiles(BaseModel): + permissive: StrictnessProfile = Field(default_factory=lambda: StrictnessProfile( + description="Focus on critical issues only", + check_security=True, + check_bugs=True, + check_style=False, + check_performance=False, + check_documentation=False, + min_severity="warning" + )) + balanced: StrictnessProfile = Field(default_factory=lambda: StrictnessProfile( + description="Balanced review of common issues", + check_security=True, + check_bugs=True, + check_style=True, + check_performance=False, + check_documentation=False, + min_severity="info" + )) + strict: StrictnessProfile = Field(default_factory=lambda: StrictnessProfile( + description="Comprehensive review of all issues", + check_security=True, + check_bugs=True, + check_style=True, + check_performance=True, + check_documentation=True, + min_severity="info" + )) + + def get_profile(self, name: str) -> StrictnessProfile: + return getattr(self, name.lower(), self.balanced) + + +class HooksConfig(BaseModel): + enabled: bool = True + fail_on_critical: bool = True + allow_bypass: bool = True + + +class OutputConfig(BaseModel): + format: str = "terminal" + theme: str = "auto" + show_suggestions: bool = True + + +class LoggingConfig(BaseModel): + level: str = "info" + log_file: str = "" + structured: bool = False + + +class Config(BaseModel): + llm: LLMConfig = Field(default_factory=LLMConfig) + review: ReviewSettings = Field(default_factory=ReviewSettings) + languages: Languages = Field(default_factory=Languages) + strictness_profiles: StrictnessProfiles = Field(default_factory=StrictnessProfiles) + hooks: HooksConfig = Field(default_factory=HooksConfig) + output: OutputConfig = Field(default_factory=OutputConfig) + logging: LoggingConfig = Field(default_factory=LoggingConfig) + + +class ConfigLoader: + def __init__(self, config_path: str | None = None): + self.config_path = config_path + self.global_config: Path | None = None + self.project_config: Path | None = None + + def find_config_files(self) -> tuple[Path | None, Path | None]: + env_config_path = os.environ.get("AICR_CONFIG_PATH") + + if env_config_path: + env_path = Path(env_config_path) + if env_path.exists(): + return env_path, None + + self.global_config = Path.home() / ".aicr.yaml" + self.project_config = Path.cwd() / ".aicr.yaml" + + if self.project_config.exists(): + return self.project_config, self.global_config + + if self.global_config.exists(): + return self.global_config, None + + return None, None + + def load(self) -> Config: + config_path, global_path = self.find_config_files() + + config_data: dict[str, Any] = {} + + if global_path and global_path.exists(): + with open(global_path) as f: + global_data = yaml.safe_load(f) or {} + config_data.update(global_data) + + if config_path and config_path.exists(): + with open(config_path) as f: + project_data = yaml.safe_load(f) or {} + config_data.update(project_data) + + return Config(**config_data) + + def save(self, config: Config, path: Path) -> None: + with open(path, "w") as f: + yaml.dump(config.model_dump(), f, default_flow_style=False) + + +def get_config(config_path: str | None = None) -> Config: + loader = ConfigLoader(config_path) + return loader.load()