44import tomllib
55from collections .abc import Sequence
66from pathlib import Path
7- from typing import Any , ClassVar , Literal , cast , get_args , overload
7+ from typing import Annotated , Any , ClassVar , Literal , Self , cast , get_args , overload
88
99from lgtm_ai .ai .schemas import AdditionalContext , CommentCategory , SupportedAIModels
10- from lgtm_ai .base .schemas import IntOrNoLimit , OutputFormat
11- from lgtm_ai .config .constants import DEFAULT_AI_MODEL , DEFAULT_INPUT_TOKEN_LIMIT
10+ from lgtm_ai .base .schemas import IntOrNoLimit , IssuesSource , OutputFormat
11+ from lgtm_ai .config .constants import DEFAULT_AI_MODEL , DEFAULT_INPUT_TOKEN_LIMIT , DEFAULT_ISSUE_REGEX
1212from lgtm_ai .config .exceptions import (
1313 ConfigFileNotFoundError ,
1414 InvalidConfigError ,
1515 InvalidConfigFileError ,
16+ InvalidOptionsError ,
1617 MissingRequiredConfigError ,
1718)
18- from pydantic import BaseModel , Field , ValidationError
19+ from lgtm_ai .config .validators import validate_regex
20+ from pydantic import AfterValidator , BaseModel , Field , HttpUrl , ValidationError , model_validator
1921
2022logger = logging .getLogger ("lgtm" )
2123
@@ -37,6 +39,9 @@ class PartialConfig(BaseModel):
3739 silent : bool = False
3840 ai_retries : int | None = None
3941 ai_input_tokens_limit : IntOrNoLimit | None = None
42+ issues_url : str | None = None
43+ issues_regex : str | None = None
44+ issues_source : IssuesSource | None = None
4045
4146 # Secrets
4247 git_api_key : str | None = None
@@ -82,13 +87,32 @@ class ResolvedConfig(BaseModel):
8287 ai_input_tokens_limit : int | None = DEFAULT_INPUT_TOKEN_LIMIT
8388 """Maximum number of input tokens allowed to send to all AI models in total."""
8489
90+ issues_url : HttpUrl | None = None
91+ """The URL of the issues page to retrieve additional context from."""
92+
93+ issues_regex : Annotated [str , AfterValidator (validate_regex )] = DEFAULT_ISSUE_REGEX
94+ """Regex to extract issue ID from the PR title and description."""
95+
96+ issues_source : IssuesSource | None = None
97+ """The platform of the issues page."""
98+
8599 # Secrets
86100 git_api_key : str = Field (default = "" , repr = False )
87101 """API key to interact with the git service (GitLab, GitHub, etc.)."""
88102
89103 ai_api_key : str = Field (default = "" , repr = False )
90104 """API key to interact with the AI model service (OpenAI, etc.)."""
91105
106+ @model_validator (mode = "after" )
107+ def validate_issues_options (self ) -> Self :
108+ all_fields = (self .issues_url , self .issues_source )
109+ if any (field is not None for field in all_fields ) and not all (field is not None for field in all_fields ):
110+ raise MissingRequiredConfigError (
111+ "If any `--issues-*` configuration is provided, all issues fields must be provided. Check --help."
112+ )
113+ # TODO: if source is JIRA, we need to ensure we have credentials
114+ return self
115+
92116
93117class ConfigHandler :
94118 """Handler for the configuration of lgtm.
@@ -143,6 +167,9 @@ def _parse_config_file(self) -> PartialConfig:
143167 silent = config_data .get ("silent" , False ),
144168 ai_retries = config_data .get ("ai_retries" , None ),
145169 ai_input_tokens_limit = config_data .get ("ai_input_tokens_limit" , None ),
170+ issues_url = config_data .get ("issues_url" , None ),
171+ issues_source = config_data .get ("issues_source" , None ),
172+ issues_regex = config_data .get ("issues_regex" , None ),
146173 )
147174 except ValidationError as err :
148175 raise InvalidConfigError (source = file_to_read .name , errors = err .errors ()) from None
@@ -203,6 +230,9 @@ def _parse_cli_args(self) -> PartialConfig:
203230 publish = self .cli_args .publish ,
204231 ai_retries = self .cli_args .ai_retries or None ,
205232 ai_input_tokens_limit = self .cli_args .ai_input_tokens_limit or None ,
233+ issues_url = self .cli_args .issues_url or None ,
234+ issues_source = self .cli_args .issues_source or None ,
235+ issues_regex = self .cli_args .issues_regex or None ,
206236 )
207237
208238 def _parse_env (self ) -> PartialConfig :
@@ -219,36 +249,42 @@ def _resolve_from_multiple_sources(
219249 self , * , from_cli : PartialConfig , from_file : PartialConfig , from_env : PartialConfig
220250 ) -> ResolvedConfig :
221251 """Resolve the config fields given all the config sources."""
222- resolved = ResolvedConfig (
223- technologies = self .resolver .resolve_tuple_field ("technologies" , from_cli = from_cli , from_file = from_file ),
224- categories = cast (
225- tuple [CommentCategory , ...],
226- self .resolver .resolve_tuple_field (
227- "categories" , from_cli = from_cli , from_file = from_file , default = get_args (CommentCategory )
252+ try :
253+ resolved = ResolvedConfig (
254+ technologies = self .resolver .resolve_tuple_field ("technologies" , from_cli = from_cli , from_file = from_file ),
255+ categories = cast (
256+ tuple [CommentCategory , ...],
257+ self .resolver .resolve_tuple_field (
258+ "categories" , from_cli = from_cli , from_file = from_file , default = get_args (CommentCategory )
259+ ),
228260 ),
229- ),
230- exclude = self .resolver .resolve_tuple_field ("exclude" , from_cli = from_cli , from_file = from_file ),
231- model = self .resolver .resolve_string_field (
232- "model" ,
233- from_cli = from_cli ,
234- from_file = from_file ,
235- required = False ,
236- default = DEFAULT_AI_MODEL ,
237- ),
238- model_url = from_cli .model_url or from_file .model_url ,
239- additional_context = from_file .additional_context or (),
240- publish = from_cli .publish or from_file .publish ,
241- output_format = from_cli .output_format or from_file .output_format or OutputFormat .pretty ,
242- silent = from_cli .silent or from_file .silent ,
243- ai_retries = from_cli .ai_retries or from_file .ai_retries ,
244- git_api_key = self .resolver .resolve_string_field ("git_api_key" , from_cli = from_cli , from_env = from_env ),
245- ai_api_key = self .resolver .resolve_string_field (
246- "ai_api_key" , from_cli = from_cli , from_env = from_env , required = False , default = ""
247- ),
248- ai_input_tokens_limit = _transform_nolimit_to_none (
249- from_cli .ai_input_tokens_limit or from_file .ai_input_tokens_limit or DEFAULT_INPUT_TOKEN_LIMIT
250- ),
251- )
261+ exclude = self .resolver .resolve_tuple_field ("exclude" , from_cli = from_cli , from_file = from_file ),
262+ model = self .resolver .resolve_string_field (
263+ "model" ,
264+ from_cli = from_cli ,
265+ from_file = from_file ,
266+ required = False ,
267+ default = DEFAULT_AI_MODEL ,
268+ ),
269+ model_url = from_cli .model_url or from_file .model_url ,
270+ additional_context = from_file .additional_context or (),
271+ publish = from_cli .publish or from_file .publish ,
272+ output_format = from_cli .output_format or from_file .output_format or OutputFormat .pretty ,
273+ silent = from_cli .silent or from_file .silent ,
274+ ai_retries = from_cli .ai_retries or from_file .ai_retries ,
275+ git_api_key = self .resolver .resolve_string_field ("git_api_key" , from_cli = from_cli , from_env = from_env ),
276+ ai_api_key = self .resolver .resolve_string_field (
277+ "ai_api_key" , from_cli = from_cli , from_env = from_env , required = False , default = ""
278+ ),
279+ ai_input_tokens_limit = _transform_nolimit_to_none (
280+ from_cli .ai_input_tokens_limit or from_file .ai_input_tokens_limit or DEFAULT_INPUT_TOKEN_LIMIT
281+ ),
282+ issues_regex = from_cli .issues_regex or from_file .issues_regex or DEFAULT_ISSUE_REGEX ,
283+ issues_url = from_cli .issues_url or from_file .issues_url ,
284+ issues_source = from_cli .issues_source or from_file .issues_source ,
285+ )
286+ except ValidationError as err :
287+ raise InvalidOptionsError (err ) from None
252288 logger .debug ("Resolved config: %s" , resolved )
253289 return resolved
254290
0 commit comments