Compare commits

..

23 Commits

Author SHA1 Message Date
7cf521c001 fix: improve null safety in GitLab webhook handler 2025-02-27 11:53:11 +02:00
Tal
e71c0f1805 Merge pull request #1586 from qodo-ai/tr/docs245
docs: add language configuration documentation and update changelog
2025-02-27 11:10:20 +02:00
8182a4afc0 docs: update default model to o3-mini and improve language configuration instructions 2025-02-27 11:06:31 +02:00
3817aa2868 fix: remove redundant temperature logging in litellm handler 2025-02-27 10:55:01 +02:00
94a8606d24 fix: update default configuration values for publishing and verbosity 2025-02-27 10:52:20 +02:00
af635650f1 fix: improve response language handling and configuration documentation 2025-02-27 10:50:28 +02:00
222f276959 docs: add language configuration documentation and update changelog 2025-02-27 10:29:05 +02:00
Tal
9a32e94b3e Merge pull request #1579 from qodo-ai/response-language-configuration
Add a language configuration to translate language generated by QodoM…
2025-02-27 08:01:13 +02:00
Tal
7c56eee701 Update README.md 2025-02-27 07:59:01 +02:00
Tal
48b3c69c10 Merge pull request #1575 from GOOD21/feature/draft-ready-trigger
gitlab webhook automatically trigger pr_command when MR changes from Draft to Ready
2025-02-27 07:57:12 +02:00
Tal
9d1c8312b5 Merge pull request #1584 from qodo-ai/tr/gitlab_fix
Tr/gitlab fix
2025-02-26 21:35:24 +02:00
64e5a87530 fix: improve error handling and null safety in PR description tool 2025-02-26 21:33:40 +02:00
9a9acef0e8 fix: handle empty commits and errors in GitLab provider's get_latest_commit_url 2025-02-26 21:24:53 +02:00
3ff8f1ff11 Add documentation 2025-02-26 18:45:24 +02:00
c7f4b87d6f Merge pull request #1583 from qodo-ai/hl/enhance_azure_devops
feat: enhance Azure DevOps integration with improved error handling a…
2025-02-26 17:17:31 +02:00
9db44b5f5f Update pr_agent/servers/azuredevops_server_webhook.py
Co-authored-by: qodo-merge-pro-for-open-source[bot] <189517486+qodo-merge-pro-for-open-source[bot]@users.noreply.github.com>
2025-02-26 17:16:07 +02:00
70a2377ac9 fix: make Azure DevOps webhook handler asynchronous 2025-02-26 17:13:38 +02:00
52a68bcd44 fix: adjust newline formatting in issue details summary 2025-02-26 16:49:44 +02:00
0a4c02c8b3 feat: enhance Azure DevOps integration with improved error handling and PR commands 2025-02-26 16:40:46 +02:00
e253f18e7f Remove default configuration 2025-02-26 14:43:56 +02:00
d6b6191f90 Remove uneccessary hardcoded configurations list 2025-02-26 14:42:32 +02:00
de80901284 Add a language configuration to translate language generated by QodoMerge, such as suggestions and desciption text 2025-02-26 14:37:15 +02:00
dfbd8dad5d Automatically trigger pr_command when MR changes from Draft to Ready 2025-02-26 18:12:48 +08:00
12 changed files with 147 additions and 289 deletions

View File

@ -52,6 +52,11 @@ PR-Agent aims to help efficiently review and handle pull requests, by providing
## News and Updates
### Feb 27, 2025
- Updated the default model to `o3-mini` for all tools. You can still use the `gpt-4o` as the default model by setting the `model` parameter in the configuration file.
- Important updates and bug fixes for Azure DevOps, see [here](https://github.com/qodo-ai/pr-agent/pull/1583)
- Added support for adjusting the [response language](https://qodo-merge-docs.qodo.ai/usage-guide/additional_configurations/#language-settings) of the PR-Agent tools.
### Feb 6, 2025
New design for the `/improve` tool:

View File

@ -57,6 +57,23 @@ All Qodo Merge tools have a parameter called `extra_instructions`, that enables
/update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..."
```
## Language Settings
The default response language for Qodo Merge is **U.S. English**. However, some development teams may prefer to display information in a different language. For example, your team's workflow might improve if PR descriptions and code suggestions are set to your country's native language.
To configure this, set the `response_language` parameter in the configuration file. This will prompt the model to respond in the specified language. Use a **standard locale code** based on [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166) (country codes) and [ISO 639](https://en.wikipedia.org/wiki/ISO_639) (language codes) to define a language-country pair. See this [comprehensive list of locale codes](https://simplelocalize.io/data/locales/).
Example:
```toml
[config]
response_language: "it-IT"
```
This will set the response language globally for all the commands to Italian.
> **Important:** Note that only dynamic text generated by the AI model is translated to the configured language. Static text such as labels and table headers that are not part of the AI models response will remain in US English. In addition, the model you are using must have good support for the specified language.
## Working with large PRs
The default mode of CodiumAI is to have a single call per tool, using GPT-4, which has a token limit of 8000 tokens.

View File

@ -44,6 +44,7 @@ command2class = {
commands = list(command2class.keys())
class PRAgent:
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
self.ai_handler = ai_handler # will be initialized in run_action
@ -72,6 +73,20 @@ class PRAgent:
# Update settings from args
args = update_settings_from_args(args)
# Append the response language in the extra instructions
response_language = get_settings().config.get('response_language', 'en-us')
if response_language.lower() != 'en-us':
get_logger().info(f'User has set the response language to: {response_language}')
for key in get_settings():
setting = get_settings().get(key)
if str(type(setting)) == "<class 'dynaconf.utils.boxing.DynaBox'>":
if hasattr(setting, 'extra_instructions'):
current_extra_instructions = setting.extra_instructions
if current_extra_instructions:
setting.extra_instructions = current_extra_instructions+ f"\n======\n\nIn addition, Your response MUST be written in the language corresponding to local code: {response_language}. This is crucial."
else:
setting.extra_instructions = f"Your response MUST be written in the language corresponding to locale code: '{response_language}'. This is crucial."
action = action.lstrip("/").lower()
if action not in command2class:
get_logger().error(f"Unknown command: {action}")

View File

@ -232,7 +232,7 @@ class LiteLLMAIHandler(BaseAiHandler):
# Add temperature only if model supports it
if model not in self.no_support_temperature_models and not get_settings().config.custom_reasoning_model:
get_logger().info(f"Adding temperature with value {temperature} to model {model}.")
# get_logger().info(f"Adding temperature with value {temperature} to model {model}.")
kwargs["temperature"] = temperature
# Add reasoning_effort if model supports it

View File

@ -250,7 +250,7 @@ def convert_to_markdown_v2(output_data: dict,
if gfm_supported:
if reference_link is not None and len(reference_link) > 0:
if relevant_lines_str:
issue_str = f"<details><summary><a href='{reference_link}'><strong>{issue_header}</strong></a>\n\n{issue_content}</summary>\n\n{relevant_lines_str}\n\n</details>"
issue_str = f"<details><summary><a href='{reference_link}'><strong>{issue_header}</strong></a>\n\n{issue_content}\n</summary>\n\n{relevant_lines_str}\n\n</details>"
else:
issue_str = f"<a href='{reference_link}'><strong>{issue_header}</strong></a><br>{issue_content}"
else:

View File

@ -183,6 +183,7 @@ class AzureDevopsProvider(GitProvider):
return True
def set_pr(self, pr_url: str):
self.pr_url = pr_url
self.workspace_slug, self.repo_slug, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr()
@ -614,8 +615,11 @@ class AzureDevopsProvider(GitProvider):
return pr_id
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to get pr id, error: {e}")
get_logger().info(f"Failed to get pr id, error: {e}")
return ""
def publish_file_comments(self, file_comments: list) -> bool:
pass
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
return self.pr_url+f"?_a=files&path={relevant_file}"

View File

@ -181,7 +181,13 @@ class GitLabProvider(GitProvider):
get_logger().exception(f"Could not update merge request {self.id_mr} description: {e}")
def get_latest_commit_url(self):
return self.mr.commits().next().web_url
try:
return self.mr.commits().next().web_url
except StopIteration: # no commits
return ""
except Exception as e:
get_logger().exception(f"Could not get latest commit URL: {e}")
return ""
def get_comment_url(self, comment):
return f"{self.mr.web_url}#note_{comment.id}"

View File

@ -33,20 +33,16 @@ azure_devops_server = get_settings().get("azure_devops_server")
WEBHOOK_USERNAME = azure_devops_server.get("webhook_username")
WEBHOOK_PASSWORD = azure_devops_server.get("webhook_password")
def handle_request(
background_tasks: BackgroundTasks, url: str, body: str, log_context: dict
async def handle_request_comment( url: str, body: str, log_context: dict
):
log_context["action"] = body
log_context["api_url"] = url
async def inner():
try:
with get_logger().contextualize(**log_context):
await PRAgent().handle_request(url, body)
except Exception as e:
get_logger().error(f"Failed to handle webhook: {e}")
background_tasks.add_task(inner)
try:
with get_logger().contextualize(**log_context):
await PRAgent().handle_request(url, body)
except Exception as e:
get_logger().exception(f"Failed to handle webhook", artifact={"url": url, "body": body}, error=str(e))
# currently only basic auth is supported with azure webhooks
@ -68,6 +64,9 @@ async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: s
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {api_url=}", **log_context)
return
commands = get_settings().get(f"azure_devops_server.{commands_conf}")
if not commands:
return
get_settings().set("config.is_auto_command", True)
for command in commands:
try:
@ -83,12 +82,7 @@ async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: s
get_logger().error(f"Failed to perform command {command}: {e}")
@router.post("/", dependencies=[Depends(authorize)])
async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context = {"server_type": "azure_devops_server"}
data = await request.json()
get_logger().info(json.dumps(data))
async def handle_request_azure(data, log_context):
actions = []
if data["eventType"] == "git.pullrequest.created":
# API V1 (latest)
@ -96,7 +90,10 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context["event"] = data["eventType"]
log_context["api_url"] = pr_url
await _perform_commands_azure("pr_commands", PRAgent(), pr_url, log_context)
return
return JSONResponse(
status_code=status.HTTP_202_ACCEPTED,
content=jsonable_encoder({"message": "webhook triggered successfully"})
)
elif data["eventType"] == "ms.vss-code.git-pullrequest-comment-event" and "content" in data["resource"]["comment"]:
if available_commands_rgx.match(data["resource"]["comment"]["content"]):
if(data["resourceVersion"] == "2.0"):
@ -124,7 +121,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
for action in actions:
try:
handle_request(background_tasks, pr_url, action, log_context)
await handle_request_comment(pr_url, action, log_context)
except Exception as e:
get_logger().error("Azure DevOps Trigger failed. Error:" + str(e))
return JSONResponse(
@ -135,6 +132,18 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder({"message": "webhook triggered successfully"})
)
@router.post("/", dependencies=[Depends(authorize)])
async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context = {"server_type": "azure_devops_server"}
data = await request.json()
# get_logger().info(json.dumps(data))
background_tasks.add_task(handle_request_azure, data, log_context)
return JSONResponse(
status_code=status.HTTP_202_ACCEPTED, content=jsonable_encoder({"message": "webhook triggered successfully"})
)
@router.get("/")
async def root():
return {"status": "ok"}

View File

@ -71,6 +71,31 @@ def is_bot_user(data) -> bool:
get_logger().error(f"Failed 'is_bot_user' logic: {e}")
return False
def is_draft(data) -> bool:
try:
if 'draft' in data.get('object_attributes', {}):
return data['object_attributes']['draft']
# for gitlab server version before 16
elif 'Draft:' in data.get('object_attributes', {}).get('title'):
return True
except Exception as e:
get_logger().error(f"Failed 'is_draft' logic: {e}")
return False
def is_draft_ready(data) -> bool:
try:
if 'draft' in data.get('changes', {}):
if data['changes']['draft']['previous'] == 'true' and data['changes']['draft']['current'] == 'false':
return True
# for gitlab server version before 16
elif 'title' in data.get('changes', {}):
if 'Draft:' in data['changes']['title']['previous'] and 'Draft:' not in data['changes']['title']['current']:
return True
except Exception as e:
get_logger().error(f"Failed 'is_draft_ready' logic: {e}")
return False
def should_process_pr_logic(data) -> bool:
try:
@ -173,23 +198,21 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
# ignore MRs based on title, labels, source and target branches
if not should_process_pr_logic(data):
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
if data['object_attributes'].get('action') in ['open', 'reopen']:
url = data['object_attributes'].get('url')
draft = data['object_attributes'].get('draft')
object_attributes = data.get('object_attributes', {})
if object_attributes.get('action') in ['open', 'reopen']:
url = object_attributes.get('url')
get_logger().info(f"New merge request: {url}")
if draft:
if is_draft(data):
get_logger().info(f"Skipping draft MR: {url}")
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
await _perform_commands_gitlab("pr_commands", PRAgent(), url, log_context, data)
# for push event triggered merge requests
elif data['object_attributes'].get('action') == 'update' and data['object_attributes'].get('oldrev'):
url = data['object_attributes'].get('url')
draft = data['object_attributes'].get('draft')
elif object_attributes.get('action') == 'update' and object_attributes.get('oldrev'):
url = object_attributes.get('url')
get_logger().info(f"New merge request: {url}")
if draft:
if is_draft(data):
get_logger().info(f"Skipping draft MR: {url}")
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
@ -202,6 +225,14 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
get_logger().debug(f'A push event has been received: {url}')
await _perform_commands_gitlab("push_commands", PRAgent(), url, log_context, data)
# for draft to ready triggered merge requests
elif object_attributes.get('action') == 'update' and is_draft_ready(data):
url = object_attributes.get('url')
get_logger().info(f"Draft MR is ready: {url}")
# same as open MR
await _perform_commands_gitlab("pr_commands", PRAgent(), url, log_context, data)
elif data.get('object_kind') == 'note' and data.get('event_type') == 'note': # comment on MR
if 'merge_request' in data:

View File

@ -1,8 +1,13 @@
# Important: This file contains all available configuration options.
# Do not copy this entire file to your repository configuration.
# Your repository configuration should only include options you wish to override from the defaults.
[config]
# models
model="gpt-4o-2024-11-20"
fallback_models=["gpt-4o-2024-08-06"]
model="o3-mini"
fallback_models=["gpt-4o-2024-11-20"]
#model_weak="gpt-4o-mini-2024-07-18" # optional, a weaker model to use for some easier tasks
response_language="en-US" # Language locales code for PR responses in ISO 3166 and ISO 639 format (e.g., "en-US", "it-IT", "zh-CN", ...)
# CLI
git_provider="github"
publish_output=true
@ -327,3 +332,11 @@ utilize_auto_best_practices = true # public - disable usage of auto best practic
extra_instructions = "" # public - extra instructions to the auto best practices generation prompt
content = ""
max_patterns = 5 # max number of patterns to be detected
[azure_devops_server]
pr_commands = [
"/describe",
"/review",
"/improve",
]

View File

@ -1,250 +0,0 @@
import copy
import json
import re
from datetime import datetime
import uvicorn
from fastapi import APIRouter, FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from starlette.background import BackgroundTasks
from starlette.middleware import Middleware
from starlette_context import context
from starlette_context.middleware import RawContextMiddleware
from pr_agent.agent.pr_agent import PRAgent
from pr_agent.algo.utils import update_settings_from_args
from pr_agent.config_loader import get_settings, global_settings
from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.secret_providers import get_secret_provider
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
router = APIRouter()
secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
async def handle_request(api_url: str, body: str, log_context: dict, sender_id: str):
log_context["action"] = body
log_context["event"] = "pull_request" if body == "/review" else "comment"
log_context["api_url"] = api_url
log_context["app_name"] = get_settings().get("CONFIG.APP_NAME", "Unknown")
with get_logger().contextualize(**log_context):
await PRAgent().handle_request(api_url, body)
async def _perform_commands_gitlab(commands_conf: str, agent: PRAgent, api_url: str,
log_context: dict, data: dict):
apply_repo_settings(api_url)
if commands_conf == "pr_commands" and get_settings().config.disable_auto_feedback: # auto commands for PR, and auto feedback is disabled
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {api_url=}", **log_context)
return
if not should_process_pr_logic(data): # Here we already updated the configurations
return
commands = get_settings().get(f"gitlab.{commands_conf}", {})
get_settings().set("config.is_auto_command", True)
for command in commands:
try:
split_command = command.split(" ")
command = split_command[0]
args = split_command[1:]
other_args = update_settings_from_args(args)
new_command = ' '.join([command] + other_args)
get_logger().info(f"Performing command: {new_command}")
with get_logger().contextualize(**log_context):
await agent.handle_request(api_url, new_command)
except Exception as e:
get_logger().error(f"Failed to perform command {command}: {e}")
def is_bot_user(data) -> bool:
try:
# logic to ignore bot users (unlike Github, no direct flag for bot users in gitlab)
sender_name = data.get("user", {}).get("name", "unknown").lower()
bot_indicators = ['codium', 'bot_', 'bot-', '_bot', '-bot']
if any(indicator in sender_name for indicator in bot_indicators):
get_logger().info(f"Skipping GitLab bot user: {sender_name}")
return True
except Exception as e:
get_logger().error(f"Failed 'is_bot_user' logic: {e}")
return False
def should_process_pr_logic(data) -> bool:
try:
if not data.get('object_attributes', {}):
return False
title = data['object_attributes'].get('title')
sender = data.get("user", {}).get("username", "")
# logic to ignore PRs from specific users
ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", [])
if ignore_pr_users and sender:
if sender in ignore_pr_users:
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' settings")
return False
# logic to ignore MRs for titles, labels and source, target branches.
ignore_mr_title = get_settings().get("CONFIG.IGNORE_PR_TITLE", [])
ignore_mr_labels = get_settings().get("CONFIG.IGNORE_PR_LABELS", [])
ignore_mr_source_branches = get_settings().get("CONFIG.IGNORE_PR_SOURCE_BRANCHES", [])
ignore_mr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", [])
#
if ignore_mr_source_branches:
source_branch = data['object_attributes'].get('source_branch')
if any(re.search(regex, source_branch) for regex in ignore_mr_source_branches):
get_logger().info(
f"Ignoring MR with source branch '{source_branch}' due to gitlab.ignore_mr_source_branches settings")
return False
if ignore_mr_target_branches:
target_branch = data['object_attributes'].get('target_branch')
if any(re.search(regex, target_branch) for regex in ignore_mr_target_branches):
get_logger().info(
f"Ignoring MR with target branch '{target_branch}' due to gitlab.ignore_mr_target_branches settings")
return False
if ignore_mr_labels:
labels = [label['title'] for label in data['object_attributes'].get('labels', [])]
if any(label in ignore_mr_labels for label in labels):
labels_str = ", ".join(labels)
get_logger().info(f"Ignoring MR with labels '{labels_str}' due to gitlab.ignore_mr_labels settings")
return False
if ignore_mr_title:
if any(re.search(regex, title) for regex in ignore_mr_title):
get_logger().info(f"Ignoring MR with title '{title}' due to gitlab.ignore_mr_title settings")
return False
except Exception as e:
get_logger().error(f"Failed 'should_process_pr_logic': {e}")
return True
@router.post("/webhook")
async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
start_time = datetime.now()
request_json = await request.json()
context["settings"] = copy.deepcopy(global_settings)
async def inner(data: dict):
log_context = {"server_type": "gitlab_app"}
get_logger().debug("Received a GitLab webhook")
if request.headers.get("X-Gitlab-Token") and secret_provider:
request_token = request.headers.get("X-Gitlab-Token")
secret = secret_provider.get_secret(request_token)
if not secret:
get_logger().warning(f"Empty secret retrieved, request_token: {request_token}")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED,
content=jsonable_encoder({"message": "unauthorized"}))
try:
secret_dict = json.loads(secret)
gitlab_token = secret_dict["gitlab_token"]
log_context["token_id"] = secret_dict.get("token_name", secret_dict.get("id", "unknown"))
context["settings"].gitlab.personal_access_token = gitlab_token
except Exception as e:
get_logger().error(f"Failed to validate secret {request_token}: {e}")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
elif get_settings().get("GITLAB.SHARED_SECRET"):
secret = get_settings().get("GITLAB.SHARED_SECRET")
if not request.headers.get("X-Gitlab-Token") == secret:
get_logger().error("Failed to validate secret")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
else:
get_logger().error("Failed to validate secret")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
if not gitlab_token:
get_logger().error("No gitlab token found")
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
get_logger().info("GitLab data", artifact=data)
sender = data.get("user", {}).get("username", "unknown")
sender_id = data.get("user", {}).get("id", "unknown")
# ignore bot users
if is_bot_user(data):
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
log_context["sender"] = sender
if data.get('object_kind') == 'merge_request':
# ignore MRs based on title, labels, source and target branches
if not should_process_pr_logic(data):
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
if data['object_attributes'].get('action') in ['open', 'reopen']:
url = data['object_attributes'].get('url')
draft = data['object_attributes'].get('draft')
get_logger().info(f"New merge request: {url}")
if draft:
get_logger().info(f"Skipping draft MR: {url}")
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
await _perform_commands_gitlab("pr_commands", PRAgent(), url, log_context, data)
# Handle the Draft to Ready transition
elif data['object_attributes'].get('action') == 'update':
url = data['object_attributes'].get('url')
old_draft_status = data['changes']['draft']['previous']
new_draft_status = data['object_attributes'].get('draft')
# Check if the merge request transitioned from Draft to Ready
if old_draft_status and not new_draft_status:
get_logger().info(f"Merge Request transitioned from Draft to Ready: {url}")
await _perform_commands_gitlab("pr_draft_ready_commands", PRAgent(), url, log_context, data)
elif data.get('object_kind') == 'note' and data.get('event_type') == 'note': # comment on MR
if 'merge_request' in data:
mr = data['merge_request']
url = mr.get('url')
get_logger().info(f"A comment has been added to a merge request: {url}")
body = data.get('object_attributes', {}).get('note')
if data.get('object_attributes', {}).get('type') == 'DiffNote' and '/ask' in body: # /ask_line
body = handle_ask_line(body, data)
await handle_request(url, body, log_context, sender_id)
background_tasks.add_task(inner, request_json)
end_time = datetime.now()
get_logger().info(f"Processing time: {end_time - start_time}", request=request_json)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
def handle_ask_line(body, data):
try:
line_range_ = data['object_attributes']['position']['line_range']
start_line = line_range_['start']['new_line']
end_line = line_range_['end']['new_line']
question = body.replace('/ask', '').strip()
path = data['object_attributes']['position']['new_path']
side = 'RIGHT'
comment_id = data['object_attributes']["discussion_id"]
get_logger().info("Handling line comment")
body = f"/ask_line --line_start={start_line} --line_end={end_line} --side={side} --file_name={path} --comment_id={comment_id} {question}"
except Exception as e:
get_logger().error(f"Failed to handle ask line comment: {e}")
return body
@router.get("/")
async def root():
return {"status": "ok"}
gitlab_url = get_settings().get("GITLAB.URL", None)
if not gitlab_url:
raise ValueError("GITLAB.URL is not set")
get_settings().config.git_provider = "gitlab"
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router)
def start():
uvicorn.run(app, host="0.0.0.0", port=3000)
if __name__ == '__main__':
start()

View File

@ -328,7 +328,10 @@ class PRDescription:
original_prediction_dict = {"pr_files": original_prediction_loaded}
else:
original_prediction_dict = original_prediction_loaded
filenames_predicted = [file['filename'].strip() for file in original_prediction_dict.get('pr_files', [])]
if original_prediction_dict:
filenames_predicted = [file.get('filename', '').strip() for file in original_prediction_dict.get('pr_files', [])]
else:
filenames_predicted = []
# extend the prediction with additional files not included in the original prediction
pr_files = self.git_provider.get_diff_files()
@ -368,8 +371,12 @@ class PRDescription:
if counter_extra_files > 0:
get_logger().info(f"Adding {counter_extra_files} unprocessed extra files to table prediction")
prediction_extra_dict = load_yaml(prediction_extra, keys_fix_yaml=self.keys_fix)
if isinstance(original_prediction_dict, dict) and isinstance(prediction_extra_dict, dict):
original_prediction_dict["pr_files"].extend(prediction_extra_dict["pr_files"])
if original_prediction_dict and isinstance(original_prediction_dict, dict) and \
isinstance(prediction_extra_dict, dict) and "pr_files" in prediction_extra_dict:
if "pr_files" in original_prediction_dict:
original_prediction_dict["pr_files"].extend(prediction_extra_dict["pr_files"])
else:
original_prediction_dict["pr_files"] = prediction_extra_dict["pr_files"]
new_yaml = yaml.dump(original_prediction_dict)
if load_yaml(new_yaml, keys_fix_yaml=self.keys_fix):
prediction = new_yaml
@ -378,7 +385,7 @@ class PRDescription:
return prediction
except Exception as e:
get_logger().error(f"Error extending uncovered files {self.pr_id}: {e}")
get_logger().exception(f"Error extending uncovered files {self.pr_id}", artifact={"error": e})
return original_prediction
@ -683,8 +690,9 @@ class PRDescription:
filename = filename.strip()
link = self.git_provider.get_line_link(filename, relevant_line_start=-1)
if (not link or not diff_plus_minus) and ('additional files' not in filename.lower()):
get_logger().warning(f"Error getting line link for '{filename}'")
continue
# get_logger().warning(f"Error getting line link for '{filename}'")
link = ""
# continue
# Add file data to the PR body
file_change_description_br = insert_br_after_x_chars(file_change_description, x=(delta - 5))