-
Notifications
You must be signed in to change notification settings - Fork 8
fix: whatever #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
fix: whatever #128
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,341 +0,0 @@ | ||
| import functools | ||
| import logging | ||
| from collections.abc import Callable | ||
| from importlib.metadata import version | ||
| from typing import Any, assert_never, get_args | ||
|
|
||
| import click | ||
| import httpx | ||
| from lgtm_ai.ai.agent import ( | ||
| get_ai_model, | ||
| get_guide_agent_with_settings, | ||
| get_reviewer_agent_with_settings, | ||
| get_summarizing_agent_with_settings, | ||
| ) | ||
| from lgtm_ai.ai.schemas import AgentSettings, CommentCategory, SupportedAIModels, SupportedAIModelsList | ||
| from lgtm_ai.base.constants import DEFAULT_HTTPX_TIMEOUT | ||
| from lgtm_ai.base.schemas import IntOrNoLimit, IssuesPlatform, OutputFormat, PRUrl | ||
| from lgtm_ai.base.utils import git_source_supports_multiline_suggestions | ||
| from lgtm_ai.config.constants import DEFAULT_INPUT_TOKEN_LIMIT | ||
| from lgtm_ai.config.handler import ConfigHandler, PartialConfig, ResolvedConfig | ||
| from lgtm_ai.formatters.base import Formatter | ||
| from lgtm_ai.formatters.json import JsonFormatter | ||
| from lgtm_ai.formatters.markdown import MarkDownFormatter | ||
| from lgtm_ai.formatters.pretty import PrettyFormatter | ||
| from lgtm_ai.git_client.base import GitClient | ||
| from lgtm_ai.git_client.utils import get_git_client | ||
| from lgtm_ai.jira.jira import JiraIssuesClient | ||
| from lgtm_ai.review import CodeReviewer | ||
| from lgtm_ai.review.context import ContextRetriever, IssuesClient | ||
| from lgtm_ai.review.guide import ReviewGuideGenerator | ||
| from lgtm_ai.validators import ( | ||
| IntOrNoLimitType, | ||
| ModelChoice, | ||
| parse_pr_url, | ||
| validate_model_url, | ||
| ) | ||
| from rich.console import Console | ||
| from rich.logging import RichHandler | ||
|
|
||
| __version__ = version("lgtm-ai") | ||
|
|
||
| logging.basicConfig( | ||
| format="%(message)s", | ||
| datefmt="[%X]", | ||
| handlers=[RichHandler(rich_tracebacks=True, show_path=False, console=Console(stderr=True))], | ||
| ) | ||
| logger = logging.getLogger("lgtm") | ||
|
|
||
|
|
||
| @click.group() | ||
| @click.version_option(__version__, "--version") | ||
| def entry_point() -> None: | ||
| pass | ||
|
|
||
|
|
||
| def _common_options[**P, T](func: Callable[P, T]) -> Callable[P, T]: | ||
| """Wrap a click command and adds common options for lgtm commands.""" | ||
|
|
||
| @click.argument("pr-url", required=True, callback=parse_pr_url) | ||
| @click.option( | ||
| "--model", | ||
| type=ModelChoice(SupportedAIModelsList), | ||
| help="The name of the model to use for the review or guide.", | ||
| ) | ||
| @click.option( | ||
| "--model-url", | ||
| type=click.STRING, | ||
| help="The URL of the custom model to use for the review or guide. Not all models support this option!", | ||
| default=None, | ||
| callback=validate_model_url, | ||
| ) | ||
| @click.option("--git-api-key", help="The API key to the git service (GitLab, GitHub, etc.)") | ||
| @click.option("--ai-api-key", help="The API key to the AI model service (OpenAI, etc.)") | ||
| @click.option("--config", type=click.STRING, help="Path to the configuration file.") | ||
| @click.option( | ||
| "--exclude", | ||
| multiple=True, | ||
| help="Exclude files from the review. If not provided, all files in the PR will be reviewed. Uses UNIX-style wildcards.", | ||
| ) | ||
| @click.option("--publish", is_flag=True, help="Publish the review or guide to the git service.") | ||
| @click.option("--output-format", type=click.Choice([format.value for format in OutputFormat])) | ||
| @click.option("--silent", is_flag=True, help="Do not print the review or guide to the console.") | ||
| @click.option( | ||
| "--ai-retries", | ||
| type=int, | ||
| help="How many times the AI agent can retry queries to the LLM (NOTE: can impact billing!).", | ||
| ) | ||
| @click.option( | ||
| "--ai-input-tokens-limit", | ||
| type=IntOrNoLimitType(), | ||
| help=f"Maximum number of input tokens allowed to send to all AI models in total (defaults to {DEFAULT_INPUT_TOKEN_LIMIT:,}). Pass 'no-limit' to disable the limit.", | ||
| ) | ||
| @click.option("--verbose", "-v", count=True, help="Set logging level.") | ||
| @functools.wraps(func) | ||
| def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: | ||
| return func(*args, **kwargs) | ||
|
|
||
| return wrapper | ||
|
|
||
|
|
||
| @entry_point.command() | ||
| @_common_options | ||
| @click.option( | ||
| "--issues-url", | ||
| type=click.STRING, | ||
| help="The URL of the issues page to retrieve additional context from. If not given, issues won't be used for reviews.", | ||
| ) | ||
| @click.option( | ||
| "--issues-platform", | ||
| type=click.Choice([source.value for source in IssuesPlatform]), | ||
| help="The platform of the issues page. If `--issues-url` is given, this is mandatory either through the CLI or config file.", | ||
| ) | ||
| @click.option( | ||
| "--issues-regex", | ||
| type=click.STRING, | ||
| help="Regex to extract issue ID from the PR title and description.", | ||
| ) | ||
| @click.option( | ||
| "--issues-api-key", | ||
| help="The optional API key to the issues platform (Jira, GitLab, GitHub, etc.). If using GitHub or GitLab and not provided, `--git-api-key` will be used instead.", | ||
| ) | ||
| @click.option( | ||
| "--issues-user", | ||
| help="The username to download issues information (only needed for Jira). Required if `--issues-platform` is `jira`.", | ||
| ) | ||
| @click.option( | ||
| "--technologies", | ||
| multiple=True, | ||
| help="List of technologies the reviewer is an expert in. If not provided, the reviewer will be an expert of all technologies in the given PR. Use it if you want to guide the reviewer to focus on specific technologies.", | ||
| ) | ||
| @click.option( | ||
| "--categories", | ||
| multiple=True, | ||
| type=click.Choice(get_args(CommentCategory)), | ||
| help="List of categories the reviewer should focus on. If not provided, the reviewer will focus on all categories.", | ||
| ) | ||
| def review( | ||
| pr_url: PRUrl, | ||
| model: SupportedAIModels | None, | ||
| model_url: str | None, | ||
| git_api_key: str | None, | ||
| ai_api_key: str | None, | ||
| config: str | None, | ||
| exclude: tuple[str, ...], | ||
| publish: bool, | ||
| output_format: OutputFormat | None, | ||
| silent: bool, | ||
| ai_retries: int | None, | ||
| ai_input_tokens_limit: IntOrNoLimit | None, | ||
| verbose: int, | ||
| technologies: tuple[str, ...], | ||
| categories: tuple[CommentCategory, ...], | ||
| issues_url: str | None, | ||
| issues_regex: str | None, | ||
| issues_platform: IssuesPlatform | None, | ||
| issues_api_key: str | None, | ||
| issues_user: str | None, | ||
| ) -> None: | ||
| """Review a Pull Request using AI. | ||
|
|
||
| PR_URL is the URL of the pull request to review. | ||
| """ | ||
| _set_logging_level(logger, verbose) | ||
|
|
||
| logger.info("lgtm-ai version: %s", __version__) | ||
| logger.debug("Parsed PR URL: %s", pr_url) | ||
| logger.info("Starting review of %s", pr_url.full_url) | ||
| resolved_config = ConfigHandler( | ||
| cli_args=PartialConfig( | ||
| technologies=technologies, | ||
| categories=categories, | ||
| exclude=exclude, | ||
| git_api_key=git_api_key, | ||
| ai_api_key=ai_api_key, | ||
| model=model, | ||
| model_url=model_url, | ||
| publish=publish, | ||
| output_format=output_format, | ||
| silent=silent, | ||
| ai_retries=ai_retries, | ||
| ai_input_tokens_limit=ai_input_tokens_limit, | ||
| issues_url=issues_url, | ||
| issues_regex=issues_regex, | ||
| issues_platform=issues_platform, | ||
| issues_api_key=issues_api_key, | ||
| issues_user=issues_user, | ||
| ), | ||
| config_file=config, | ||
| ).resolve_config() | ||
| agent_extra_settings = AgentSettings(retries=resolved_config.ai_retries) | ||
| formatter: Formatter[Any] = MarkDownFormatter( | ||
| add_ranges_to_suggestions=git_source_supports_multiline_suggestions(pr_url.source) | ||
| ) | ||
| git_client = get_git_client(source=pr_url.source, token=resolved_config.git_api_key, formatter=formatter) | ||
| issues_client = _get_issues_client(resolved_config, git_client, formatter) | ||
|
|
||
| code_reviewer = CodeReviewer( | ||
| reviewer_agent=get_reviewer_agent_with_settings(agent_extra_settings), | ||
| summarizing_agent=get_summarizing_agent_with_settings(agent_extra_settings), | ||
| model=get_ai_model( | ||
| model_name=resolved_config.model, api_key=resolved_config.ai_api_key, model_url=resolved_config.model_url | ||
| ), | ||
| context_retriever=ContextRetriever( | ||
| git_client=git_client, issues_client=issues_client, httpx_client=httpx.Client(timeout=DEFAULT_HTTPX_TIMEOUT) | ||
| ), | ||
| git_client=git_client, | ||
| config=resolved_config, | ||
| ) | ||
| review = code_reviewer.review_pull_request(pr_url=pr_url) | ||
| logger.info("Review completed, total comments: %d", len(review.review_response.comments)) | ||
|
|
||
| if not resolved_config.silent: | ||
| logger.info("Printing review to console") | ||
| formatter, printer = _get_formatter_and_printer(resolved_config.output_format) | ||
| printer(formatter.format_review_summary_section(review)) | ||
| if review.review_response.comments: | ||
| printer(formatter.format_review_comments_section(review.review_response.comments)) | ||
|
|
||
| if resolved_config.publish: | ||
| logger.info("Publishing review to git service") | ||
| git_client.publish_review(pr_url=pr_url, review=review) | ||
| logger.info("Review published successfully") | ||
|
|
||
|
|
||
| @entry_point.command() | ||
| @_common_options | ||
| def guide( | ||
| pr_url: PRUrl, | ||
| model: SupportedAIModels | None, | ||
| model_url: str | None, | ||
| git_api_key: str | None, | ||
| ai_api_key: str | None, | ||
| config: str | None, | ||
| exclude: tuple[str, ...], | ||
| publish: bool, | ||
| output_format: OutputFormat | None, | ||
| silent: bool, | ||
| ai_retries: int | None, | ||
| ai_input_tokens_limit: IntOrNoLimit | None, | ||
| verbose: int, | ||
| ) -> None: | ||
| """Generate a review guide for a Pull Request using AI. | ||
|
|
||
| PR_URL is the URL of the pull request to generate a guide for. | ||
| """ | ||
| _set_logging_level(logger, verbose) | ||
|
|
||
| logger.info("lgtm-ai version: %s", __version__) | ||
| logger.debug("Parsed PR URL: %s", pr_url) | ||
| logger.info("Starting generating guide of %s", pr_url.full_url) | ||
| resolved_config = ConfigHandler( | ||
| cli_args=PartialConfig( | ||
| exclude=exclude, | ||
| git_api_key=git_api_key, | ||
| ai_api_key=ai_api_key, | ||
| model=model, | ||
| model_url=model_url, | ||
| publish=publish, | ||
| output_format=output_format, | ||
| silent=silent, | ||
| ai_retries=ai_retries, | ||
| ai_input_tokens_limit=ai_input_tokens_limit, | ||
| ), | ||
| config_file=config, | ||
| ).resolve_config() | ||
| agent_extra_settings = AgentSettings(retries=resolved_config.ai_retries) | ||
| git_client = get_git_client(source=pr_url.source, token=resolved_config.git_api_key, formatter=MarkDownFormatter()) | ||
| review_guide = ReviewGuideGenerator( | ||
| guide_agent=get_guide_agent_with_settings(agent_extra_settings), | ||
| model=get_ai_model( | ||
| model_name=resolved_config.model, api_key=resolved_config.ai_api_key, model_url=resolved_config.model_url | ||
| ), | ||
| git_client=git_client, | ||
| config=resolved_config, | ||
| ) | ||
| guide = review_guide.generate_review_guide(pr_url=pr_url) | ||
|
|
||
| if not resolved_config.silent: | ||
| logger.info("Printing review to console") | ||
| formatter, printer = _get_formatter_and_printer(resolved_config.output_format) | ||
| printer(formatter.format_guide(guide)) | ||
|
|
||
| if resolved_config.publish: | ||
| logger.info("Publishing review guide to git service") | ||
| git_client.publish_guide(pr_url=pr_url, guide=guide) | ||
| logger.info("Review Guide published successfully") | ||
|
|
||
|
|
||
| def _set_logging_level(logger: logging.Logger, verbose: int) -> None: | ||
| if verbose == 0: | ||
| logger.setLevel(logging.ERROR) | ||
| elif verbose == 1: | ||
| logger.setLevel(logging.INFO) | ||
| else: | ||
| logger.setLevel(logging.DEBUG) | ||
| logger.info("Logging level set to %s", logging.getLevelName(logger.level)) | ||
|
|
||
|
|
||
| def _get_formatter_and_printer(output_format: OutputFormat) -> tuple[Formatter[Any], Callable[[Any], None]]: | ||
| """Get the formatter and the print method based on the output format.""" | ||
| if output_format == OutputFormat.pretty: | ||
| console = Console() | ||
| return PrettyFormatter(), console.print | ||
| elif output_format == OutputFormat.markdown: | ||
| return MarkDownFormatter(), print | ||
| elif output_format == OutputFormat.json: | ||
| return JsonFormatter(), print | ||
| else: | ||
| assert_never(output_format) | ||
|
|
||
|
|
||
| def _get_issues_client( | ||
| resolved_config: ResolvedConfig, git_client: GitClient, formatter: Formatter[Any] | ||
| ) -> IssuesClient: | ||
| """Select a different issues client for retrieving issues. | ||
|
|
||
| Will only return a different client if all of the following are true: | ||
| 1) Be used at all | ||
| 2) Be retrieved from a git platform and not elsewhere (e.g., Jira, Asana, etc.) | ||
| 3) Have a specific API key configured | ||
| """ | ||
| issues_client: IssuesClient = git_client | ||
| if not resolved_config.issues_url or not resolved_config.issues_platform or not resolved_config.issues_regex: | ||
| return issues_client | ||
| if resolved_config.issues_platform.is_git_platform: | ||
| if resolved_config.issues_api_key: | ||
| issues_client = get_git_client( | ||
| source=resolved_config.issues_platform, token=resolved_config.issues_api_key, formatter=formatter | ||
| ) | ||
| elif resolved_config.issues_platform == IssuesPlatform.jira: | ||
| if not resolved_config.issues_api_key or not resolved_config.issues_user: | ||
| # This is validated earlier in config handler. | ||
| raise ValueError("To use Jira as issues source, both `issues_user` and `issues_api_key` must be provided.") | ||
| issues_client = JiraIssuesClient( | ||
| issues_user=resolved_config.issues_user, | ||
| issues_api_key=resolved_config.issues_api_key, | ||
| httpx_client=httpx.Client(timeout=DEFAULT_HTTPX_TIMEOUT), | ||
| ) | ||
| else: | ||
| raise NotImplementedError("Unsupported issues source") | ||
| return issues_client | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🦉 🎯 Correctness
The entire content of this file has been deleted. This file defines the command-line interface and is the main entry point for the application. Removing its content will completely break the tool. This seems like a mistake. Can you please verify if this change was intentional?
More information about this comment
src/lgtm_ai/__main__.py11NoNo