diff --git a/rhodecode/apps/_base/navigation.py b/rhodecode/apps/_base/navigation.py index a9add214..73b87de8 100644 --- a/rhodecode/apps/_base/navigation.py +++ b/rhodecode/apps/_base/navigation.py @@ -72,6 +72,7 @@ class NavEntry(object): class NavigationRegistry(object): _base_entries = [ NavEntry("global", _("Global"), "admin_settings_global"), + NavEntry("admin_settings_ai", _("Ai"), "admin_settings_ai"), NavEntry("vcs", _("VCS"), "admin_settings_vcs"), NavEntry("visual", _("Visual"), "admin_settings_visual"), NavEntry("mapping", _("Remap and Rescan"), "admin_settings_mapping"), diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py index 88ce4960..3cf1b7e3 100644 --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -28,6 +28,7 @@ def admin_routes(config): """ from rhodecode.apps.admin.views.audit_logs import AdminAuditLogsView from rhodecode.apps.admin.views.artifacts import AdminArtifactsView + from rhodecode.apps.admin.views.ai import AdminAiView from rhodecode.apps.admin.views.automation import AdminAutomationView from rhodecode.apps.admin.views.scheduler import AdminSchedulerView from rhodecode.apps.admin.views.defaults import AdminDefaultSettingsView @@ -147,6 +148,24 @@ def admin_routes(config): renderer="rhodecode:templates/admin/settings/settings.mako", ) + config.add_route("admin_settings_ai", pattern="/settings/ai") + config.add_view( + AdminAiView, + attr="admin_settings_ai", + route_name="admin_settings_ai", + request_method="GET", + renderer="rhodecode:templates/admin/settings/settings.mako", + ) + + config.add_route(name="admin_settings_ai_update", pattern="/settings/ai/update") + config.add_view( + AdminAiView, + attr="admin_settings_ai_update", + route_name="admin_settings_ai_update", + request_method="POST", + renderer="rhodecode:templates/admin/settings/settings.mako", + ) + config.add_route(name="admin_settings_vcs_svn_generate_cfg", pattern="/settings/vcs/svn_generate_cfg") config.add_view( AdminSvnConfigView, diff --git a/rhodecode/apps/admin/views/ai.py b/rhodecode/apps/admin/views/ai.py new file mode 100644 index 00000000..f95960b1 --- /dev/null +++ b/rhodecode/apps/admin/views/ai.py @@ -0,0 +1,79 @@ +import logging +import formencode + +from pyramid.httpexceptions import HTTPFound +from rhodecode.apps._base import BaseAppView +from rhodecode.apps._base.navigation import navigation_list +from rhodecode.apps.ai_agents.ai_settings import AIModelName, GPTVersion, ClaudeVersion +from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, CSRFRequired +from rhodecode.lib import helpers as h +from rhodecode.model.forms import AiSettingsForm +from rhodecode.model.settings import SettingsModel +from rhodecode.model.meta import Session + +log = logging.getLogger(__name__) + + +class AdminAiView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + return c + + @LoginRequired() + @HasPermissionAllDecorator("hg.admin") + def admin_settings_ai(self): + c = self.load_default_context() + c.active = "ai" + c.navlist = navigation_list(self.request) + + app_settings = c.rc_config + c.selected_ai_model = app_settings.get("rhodecode_ai_model", AIModelName.GPT.name) + c.selected_ai_model_version = app_settings.get("rhodecode_ai_model_version", GPTVersion.V5_nano.name) + c.api_key = app_settings.get("rhodecode_ai_api_key") + + c.model_map = { + AIModelName.GPT.name: [v.name for v in GPTVersion], + } + return self._get_template_context(c) + + @CSRFRequired() + @LoginRequired() + @HasPermissionAllDecorator("hg.admin") + def admin_settings_ai_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = "ai" + + data = self._parse_form(_) + self._save_settings(_, data) + + raise HTTPFound(h.route_path("admin_settings_ai")) + + def _save_settings(self, _, data): + try: + settings = [ + ("ai_model", "rhodecode_ai_model", "unicode"), + ("ai_model_version", "rhodecode_ai_model_version", "unicode"), + ("ai_api_key", "rhodecode_ai_api_key", "unicode"), + ] + for setting, form_key, type_ in settings: + sett = SettingsModel().create_or_update_setting(setting, data[form_key], type_) + Session().add(sett) + + Session().commit() + SettingsModel().invalidate_settings_cache() + h.flash(_("AI settings saved"), category="success") + except Exception as e: + log.exception("Exception saving AI settings: %s", e) + h.flash(_("Error occurred during saving AI settings"), category="error") + + def _parse_form(self, _): + try: + form = AiSettingsForm()() + data = form.to_python(self.request.POST) + except formencode.Invalid as errors: + log.exception("Failed to add new pattern") + error = errors + h.flash(_(f"Unknown error: {error}"), category="error") + raise HTTPFound(h.route_path("admin_settings_ai")) + return data diff --git a/rhodecode/apps/ai_agents/ai_settings.py b/rhodecode/apps/ai_agents/ai_settings.py index 0b9bd6fb..876ce74b 100644 --- a/rhodecode/apps/ai_agents/ai_settings.py +++ b/rhodecode/apps/ai_agents/ai_settings.py @@ -4,6 +4,11 @@ from dataclasses import dataclass class AIModelName(enum.StrEnum): GPT = enum.auto() + Claude = enum.auto() + + +class ClaudeVersion(enum.StrEnum): + Opus = "opus" class GPTVersion(enum.StrEnum): diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py index bcf35235..f4b3e191 100644 --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -666,3 +666,13 @@ def IssueTrackerPatternsForm(localizer): chained_validators = [v.ValidPattern(localizer)] return _IssueTrackerPatternsForm + + +def AiSettingsForm(): + class _AiSettingsForm(formencode.Schema): + allow_extra_fields = True + rhodecode_ai_model = v.UnicodeString(strip=True, required=True) + rhodecode_ai_model_version = v.UnicodeString(strip=True, required=True) + rhodecode_ai_api_key = v.UnicodeString(strip=True) + + return _AiSettingsForm diff --git a/rhodecode/templates/admin/settings/settings_ai.mako b/rhodecode/templates/admin/settings/settings_ai.mako new file mode 100644 index 00000000..074bf92e --- /dev/null +++ b/rhodecode/templates/admin/settings/settings_ai.mako @@ -0,0 +1,107 @@ +