poc: POC fully integrated, need still to fix a couple of bugs and add audit logs
This commit is contained in:
parent
3d0a1b146a
commit
29564261fb
4 changed files with 182 additions and 15 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue