poc: POC fully integrated, need still to fix a couple of bugs and add audit logs

This commit is contained in:
ievgenii vdovenko 2025-09-03 13:08:19 +02:00
parent 3d0a1b146a
commit 29564261fb
4 changed files with 182 additions and 15 deletions

View file

@ -4,15 +4,18 @@ from rhodecode.apps.ai_agents.models.gpt import GPTService
@wrap_ai_exceptions
def get_ai_service(api_key: str, model_name=AIModelName.GPT, version=GPTVersion.V5_nano):
def get_ai_service(api_key: str, model_name: str = AIModelName.GPT.name, version: str = GPTVersion.V5_nano.name):
assert api_key, "API key is required"
if model_name is AIModelName.GPT:
return GPTService(
AISettings(
model_name=AIModelName.GPT,
model_version=version,
api_key=api_key,
try:
if AIModelName[model_name] is AIModelName.GPT:
return GPTService(
AISettings(
model_name=AIModelName.GPT,
model_version=GPTVersion[version],
api_key=api_key,
)
)
)
except KeyError as ke:
raise ValueError("Unknown model or version: %s" % ke)
raise ValueError("Unknown model name: %s" % model_name)

View file

@ -1,6 +1,7 @@
from abc import abstractmethod
from dataclasses import dataclass
from functools import wraps
from typing import Optional, Iterable
from rhodecode.apps.ai_agents.ai_settings import AISettings
from rhodecode.lib.codeblocks import DiffSet
@ -74,7 +75,7 @@ class AIServiceBase:
@wrap_ai_exceptions
def code_review(self, pr_diffset: DiffSet, *args, **kwargs) -> Response:
resp = self._get_response(self._get_review_requests(pr_diffset))
resp = self._get_response(self._get_review_requests(pr_diffset, *args, **kwargs))
return self._transform(resp)
def get_code_review_instructions(self, basic_points, review_content, custom_instructions=None):
@ -120,7 +121,14 @@ class AIServiceBase:
raise NotImplementedError
@abstractmethod
def _get_review_requests(self, pr_diffset: DiffSet, *args, **kwargs) -> list[Request]:
def _get_review_requests(
self,
pr_diffset: DiffSet,
basic_instructions: Optional[Iterable[str]] = None,
custom_instructions: Optional[str] = None,
*args,
**kwargs,
) -> list[Request]:
raise NotImplementedError
@abstractmethod

View file

@ -30,6 +30,7 @@ from rhodecode.apps._base import RepoAppView, DataGridAppView
from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
from rhodecode.lib.base import vcs_operation_context
from rhodecode.lib.celerylib import run_task, tasks
from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
from rhodecode.lib.exceptions import CommentVersionMismatch
from rhodecode.lib import ext_json
@ -54,6 +55,7 @@ from rhodecode.model.db import (
ChangesetStatus,
Repository,
PullRequestReviewers,
User,
)
from rhodecode.model.forms import PullRequestForm
from rhodecode.model.meta import Session
@ -1310,13 +1312,20 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
do_update_branch_close = "close_branch_before_merging" in self.request.POST
if "review_members" in controls:
self._update_reviewers(
ai_user = User.get_ai_user()
ai_user_id = ai_user.user_id
ids_to_add, ids_to_remove = self._update_reviewers(
c,
pull_request,
controls["review_members"],
pull_request.reviewer_data,
PullRequestReviewers.ROLE_REVIEWER,
)
if ai_user_id in ids_to_add:
log.debug("Initiating code review for pull request id: %s", pull_request.pull_request_id)
run_task(tasks.start_ai_code_review, pull_request.pull_request_id)
elif "observer_members" in controls:
self._update_reviewers(
c,
@ -1438,6 +1447,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
PullRequestModel().get_reviewer_functions()
)
ids_to_add = None
ids_to_remove = None
if role == PullRequestReviewers.ROLE_REVIEWER:
try:
reviewers = validate_default_reviewers(review_members, reviewer_rules)
@ -1447,7 +1459,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
return
old_calculated_status = pull_request.calculated_review_status()
PullRequestModel().update_reviewers(pull_request, reviewers, self._rhodecode_db_user)
ids_to_add, ids_to_remove = PullRequestModel().update_reviewers(
pull_request, reviewers, self._rhodecode_db_user
)
Session().commit()
@ -1470,13 +1484,17 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
h.flash(e, category="error")
return
PullRequestModel().update_observers(pull_request, observers, self._rhodecode_db_user)
ids_to_add, ids_to_remove = PullRequestModel().update_observers(
pull_request, observers, self._rhodecode_db_user
)
Session().commit()
msg = _("Pull request observers updated.")
h.flash(msg, category="success")
channelstream.pr_update_channelstream_push(self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
return ids_to_add, ids_to_remove
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")

View file

@ -30,14 +30,30 @@ from pyramid_mailer.message import Message
from email.utils import formatdate
import rhodecode
from rhodecode.lib import audit_logger
from rhodecode.apps.ai_agents.ai_service import get_ai_service
from rhodecode.lib import audit_logger, diffs, codeblocks
from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask, run_task
from rhodecode.lib import hooks_base
from rhodecode.lib.diffs import MAX_CONTEXT
from rhodecode.lib.str_utils import safe_str
from rhodecode.lib.utils import adopt_for_celery
from rhodecode.lib.utils2 import safe_int, str2bool, aslist
from rhodecode.lib.statsd_client import StatsdClient
from rhodecode.model.db import true, null, Session, IntegrityError, Repository, RepoGroup, User, ScheduleEntry
from rhodecode.model.comment import CommentsModel
from rhodecode.model.db import (
true,
null,
Session,
IntegrityError,
Repository,
RepoGroup,
User,
ScheduleEntry,
PullRequest,
)
from rhodecode.model.permission import PermissionModel
from rhodecode.model.pull_request import PullRequestModel
from rhodecode.model.settings import SettingsModel
@async_task(ignore_result=True, base=RequestContextTask)
@ -459,6 +475,128 @@ def unschedule_sync_ldap_ad_users_producer():
Session().delete(existing_task)
@async_task
def start_ai_code_review(pull_request_id):
log = get_logger(start_ai_code_review)
log.info("Starting AI code review for pull request id: %s", pull_request_id)
rc_settings = SettingsModel()
ai_enabled = rc_settings.get_setting_by_name("ai_features_enabled")
if not ai_enabled:
log.info("AI code review is disabled.")
return
pull_request: PullRequest = PullRequest.get(pull_request_id)
if not pull_request:
log.info("Pull request id: %s not found, review not possible.", pull_request_id)
return
if pull_request.is_state_changing():
log.info("Pull request id: %s is in state changing, review not possible.", pull_request_id)
return
settings_valid = _validate_settings(rc_settings, log)
if not settings_valid:
return
diffset = _get_diffset(pull_request)
ai_model = rc_settings.get_setting_by_name("ai_model").app_settings_value
ai_model_version = rc_settings.get_setting_by_name("ai_model_version").app_settings_value
ai_api_key = rc_settings.get_setting_by_name("ai_api_key").app_settings_value
if not ai_model or not ai_model or not ai_api_key:
log.info("AI model or model version or API key is not set, review not possible.")
return
service = get_ai_service(
api_key=ai_api_key,
model_name=ai_model,
version=ai_model_version,
)
instructions = rc_settings.get_setting_by_name("ai_code_review_instructions")
if instructions:
instructions = instructions.app_settings_value.split("\r\n")
response = service.code_review(diffset, instructions=instructions)
log.debug(
"Response from AI service: %s",
response.message,
)
for response_line in response.message.get("response", []):
file_name = response_line.get("file_name")
review = response_line.get("review", [])
log.debug("Review for file: %s", file_name)
for review_line in review:
line_number = review_line.get("line_number")
line_code = review_line.get("line_code")
suggestion = review_line.get("suggestion")
log.debug("%s | %s | %s", line_number, line_code, suggestion)
insert_comment(line_number, suggestion, file_name, pull_request)
def insert_comment(line_number: int, text: str, f_path: str, pull_request):
target_repo: Repository = pull_request.target_repo
repo_id = target_repo.repo_id
line_number_formatted = "n%s" % line_number
ai_user = User.get_ai_user()
comment_type = "note"
CommentsModel().create(
text=text,
repo=repo_id,
user=ai_user.user_id,
pull_request=pull_request,
f_path=f_path,
line_no=line_number_formatted,
comment_type=comment_type,
send_email=False,
)
def _validate_settings(rc_settings, log):
if not rc_settings.get_setting_by_name("ai_api_key"):
log.info("AI API key is not set, review not possible.")
return False
if not rc_settings.get_setting_by_name("ai_model"):
log.info("AI model is not set, review not possible.")
return False
if not rc_settings.get_setting_by_name("ai_model_version"):
log.info("AI model version is not set, review not possible.")
return False
return True
def _get_diffset(
pull_request: PullRequest,
):
source_repo: Repository = pull_request.source_repo
target_repo: Repository = pull_request.target_repo
source_ref_id = pull_request.source_ref_parts.commit_id
target_ref_id = pull_request.target_ref_parts.commit_id
pre_load = ["author", "date", "message", "branch", "parents"]
target_commit_final = target_repo.get_commit(commit_id=safe_str(target_ref_id), pre_load=pre_load)
source_commit_final = source_repo.get_commit(commit_id=safe_str(source_ref_id), pre_load=pre_load)
vcs_diff = PullRequestModel().get_diff(source_repo, source_ref_id, target_ref_id, False, MAX_CONTEXT)
diff_processor = diffs.DiffProcessor(vcs_diff, diff_format="newdiff")
_parsed = diff_processor.prepare()
diffset = codeblocks.DiffSet(
repo_name=target_repo.repo_name,
source_repo_name=source_repo.repo_name,
source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
)
return diffset.render_patchset(_parsed, source_ref=source_ref_id, target_ref=target_ref_id)
@async_task
@adopt_for_celery
def repo_size(extras):