xai-review 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xai-review might be problematic. Click here for more details.
- ai_review/__init__.py +0 -0
- ai_review/cli/__init__.py +0 -0
- ai_review/cli/commands/__init__.py +0 -0
- ai_review/cli/commands/run_context_review.py +7 -0
- ai_review/cli/commands/run_inline_review.py +7 -0
- ai_review/cli/commands/run_review.py +8 -0
- ai_review/cli/commands/run_summary_review.py +7 -0
- ai_review/cli/main.py +54 -0
- ai_review/clients/__init__.py +0 -0
- ai_review/clients/claude/__init__.py +0 -0
- ai_review/clients/claude/client.py +44 -0
- ai_review/clients/claude/schema.py +44 -0
- ai_review/clients/gemini/__init__.py +0 -0
- ai_review/clients/gemini/client.py +45 -0
- ai_review/clients/gemini/schema.py +78 -0
- ai_review/clients/gitlab/__init__.py +0 -0
- ai_review/clients/gitlab/client.py +31 -0
- ai_review/clients/gitlab/mr/__init__.py +0 -0
- ai_review/clients/gitlab/mr/client.py +101 -0
- ai_review/clients/gitlab/mr/schema/__init__.py +0 -0
- ai_review/clients/gitlab/mr/schema/changes.py +35 -0
- ai_review/clients/gitlab/mr/schema/comments.py +19 -0
- ai_review/clients/gitlab/mr/schema/discussions.py +34 -0
- ai_review/clients/openai/__init__.py +0 -0
- ai_review/clients/openai/client.py +42 -0
- ai_review/clients/openai/schema.py +37 -0
- ai_review/config.py +62 -0
- ai_review/libs/__init__.py +0 -0
- ai_review/libs/asynchronous/__init__.py +0 -0
- ai_review/libs/asynchronous/gather.py +14 -0
- ai_review/libs/config/__init__.py +0 -0
- ai_review/libs/config/artifacts.py +12 -0
- ai_review/libs/config/base.py +24 -0
- ai_review/libs/config/claude.py +13 -0
- ai_review/libs/config/gemini.py +13 -0
- ai_review/libs/config/gitlab.py +12 -0
- ai_review/libs/config/http.py +19 -0
- ai_review/libs/config/llm.py +61 -0
- ai_review/libs/config/logger.py +17 -0
- ai_review/libs/config/openai.py +13 -0
- ai_review/libs/config/prompt.py +121 -0
- ai_review/libs/config/review.py +30 -0
- ai_review/libs/config/vcs.py +19 -0
- ai_review/libs/constants/__init__.py +0 -0
- ai_review/libs/constants/llm_provider.py +7 -0
- ai_review/libs/constants/vcs_provider.py +6 -0
- ai_review/libs/diff/__init__.py +0 -0
- ai_review/libs/diff/models.py +100 -0
- ai_review/libs/diff/parser.py +111 -0
- ai_review/libs/diff/tools.py +24 -0
- ai_review/libs/http/__init__.py +0 -0
- ai_review/libs/http/client.py +14 -0
- ai_review/libs/http/event_hooks/__init__.py +0 -0
- ai_review/libs/http/event_hooks/base.py +13 -0
- ai_review/libs/http/event_hooks/logger.py +17 -0
- ai_review/libs/http/handlers.py +34 -0
- ai_review/libs/http/transports/__init__.py +0 -0
- ai_review/libs/http/transports/retry.py +34 -0
- ai_review/libs/logger.py +19 -0
- ai_review/libs/resources.py +24 -0
- ai_review/prompts/__init__.py +0 -0
- ai_review/prompts/default_context.md +14 -0
- ai_review/prompts/default_inline.md +8 -0
- ai_review/prompts/default_summary.md +3 -0
- ai_review/prompts/default_system_context.md +27 -0
- ai_review/prompts/default_system_inline.md +25 -0
- ai_review/prompts/default_system_summary.md +7 -0
- ai_review/resources/__init__.py +0 -0
- ai_review/resources/pricing.yaml +55 -0
- ai_review/services/__init__.py +0 -0
- ai_review/services/artifacts/__init__.py +0 -0
- ai_review/services/artifacts/schema.py +11 -0
- ai_review/services/artifacts/service.py +47 -0
- ai_review/services/artifacts/tools.py +8 -0
- ai_review/services/cost/__init__.py +0 -0
- ai_review/services/cost/schema.py +44 -0
- ai_review/services/cost/service.py +58 -0
- ai_review/services/diff/__init__.py +0 -0
- ai_review/services/diff/renderers.py +149 -0
- ai_review/services/diff/schema.py +6 -0
- ai_review/services/diff/service.py +96 -0
- ai_review/services/diff/tools.py +59 -0
- ai_review/services/git/__init__.py +0 -0
- ai_review/services/git/service.py +35 -0
- ai_review/services/git/types.py +11 -0
- ai_review/services/llm/__init__.py +0 -0
- ai_review/services/llm/claude/__init__.py +0 -0
- ai_review/services/llm/claude/client.py +26 -0
- ai_review/services/llm/factory.py +18 -0
- ai_review/services/llm/gemini/__init__.py +0 -0
- ai_review/services/llm/gemini/client.py +31 -0
- ai_review/services/llm/openai/__init__.py +0 -0
- ai_review/services/llm/openai/client.py +28 -0
- ai_review/services/llm/types.py +15 -0
- ai_review/services/prompt/__init__.py +0 -0
- ai_review/services/prompt/adapter.py +25 -0
- ai_review/services/prompt/schema.py +71 -0
- ai_review/services/prompt/service.py +56 -0
- ai_review/services/review/__init__.py +0 -0
- ai_review/services/review/inline/__init__.py +0 -0
- ai_review/services/review/inline/schema.py +53 -0
- ai_review/services/review/inline/service.py +38 -0
- ai_review/services/review/policy/__init__.py +0 -0
- ai_review/services/review/policy/service.py +60 -0
- ai_review/services/review/service.py +207 -0
- ai_review/services/review/summary/__init__.py +0 -0
- ai_review/services/review/summary/schema.py +15 -0
- ai_review/services/review/summary/service.py +14 -0
- ai_review/services/vcs/__init__.py +0 -0
- ai_review/services/vcs/factory.py +12 -0
- ai_review/services/vcs/gitlab/__init__.py +0 -0
- ai_review/services/vcs/gitlab/client.py +152 -0
- ai_review/services/vcs/types.py +55 -0
- ai_review/tests/__init__.py +0 -0
- ai_review/tests/fixtures/__init__.py +0 -0
- ai_review/tests/fixtures/git.py +31 -0
- ai_review/tests/suites/__init__.py +0 -0
- ai_review/tests/suites/clients/__init__.py +0 -0
- ai_review/tests/suites/clients/claude/__init__.py +0 -0
- ai_review/tests/suites/clients/claude/test_client.py +31 -0
- ai_review/tests/suites/clients/claude/test_schema.py +59 -0
- ai_review/tests/suites/clients/gemini/__init__.py +0 -0
- ai_review/tests/suites/clients/gemini/test_client.py +30 -0
- ai_review/tests/suites/clients/gemini/test_schema.py +105 -0
- ai_review/tests/suites/clients/openai/__init__.py +0 -0
- ai_review/tests/suites/clients/openai/test_client.py +30 -0
- ai_review/tests/suites/clients/openai/test_schema.py +53 -0
- ai_review/tests/suites/libs/__init__.py +0 -0
- ai_review/tests/suites/libs/diff/__init__.py +0 -0
- ai_review/tests/suites/libs/diff/test_models.py +105 -0
- ai_review/tests/suites/libs/diff/test_parser.py +115 -0
- ai_review/tests/suites/libs/diff/test_tools.py +62 -0
- ai_review/tests/suites/services/__init__.py +0 -0
- ai_review/tests/suites/services/diff/__init__.py +0 -0
- ai_review/tests/suites/services/diff/test_renderers.py +168 -0
- ai_review/tests/suites/services/diff/test_service.py +84 -0
- ai_review/tests/suites/services/diff/test_tools.py +108 -0
- ai_review/tests/suites/services/prompt/__init__.py +0 -0
- ai_review/tests/suites/services/prompt/test_schema.py +38 -0
- ai_review/tests/suites/services/prompt/test_service.py +128 -0
- ai_review/tests/suites/services/review/__init__.py +0 -0
- ai_review/tests/suites/services/review/inline/__init__.py +0 -0
- ai_review/tests/suites/services/review/inline/test_schema.py +65 -0
- ai_review/tests/suites/services/review/inline/test_service.py +49 -0
- ai_review/tests/suites/services/review/policy/__init__.py +0 -0
- ai_review/tests/suites/services/review/policy/test_service.py +95 -0
- ai_review/tests/suites/services/review/summary/__init__.py +0 -0
- ai_review/tests/suites/services/review/summary/test_schema.py +22 -0
- ai_review/tests/suites/services/review/summary/test_service.py +16 -0
- xai_review-0.3.0.dist-info/METADATA +11 -0
- xai_review-0.3.0.dist-info/RECORD +154 -0
- xai_review-0.3.0.dist-info/WHEEL +5 -0
- xai_review-0.3.0.dist-info/entry_points.txt +2 -0
- xai_review-0.3.0.dist-info/top_level.txt +1 -0
ai_review/config.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from pydantic_settings import (
|
|
2
|
+
BaseSettings,
|
|
3
|
+
SettingsConfigDict,
|
|
4
|
+
YamlConfigSettingsSource,
|
|
5
|
+
JsonConfigSettingsSource,
|
|
6
|
+
PydanticBaseSettingsSource
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ai_review.libs.config.artifacts import ArtifactsConfig
|
|
10
|
+
from ai_review.libs.config.base import (
|
|
11
|
+
get_env_config_file_or_default,
|
|
12
|
+
get_yaml_config_file_or_default,
|
|
13
|
+
get_json_config_file_or_default
|
|
14
|
+
)
|
|
15
|
+
from ai_review.libs.config.llm import LLMConfig
|
|
16
|
+
from ai_review.libs.config.logger import LoggerConfig
|
|
17
|
+
from ai_review.libs.config.prompt import PromptConfig
|
|
18
|
+
from ai_review.libs.config.review import ReviewConfig
|
|
19
|
+
from ai_review.libs.config.vcs import VCSConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Settings(BaseSettings):
|
|
23
|
+
model_config = SettingsConfigDict(
|
|
24
|
+
extra='allow',
|
|
25
|
+
|
|
26
|
+
env_file=get_env_config_file_or_default(),
|
|
27
|
+
env_file_encoding="utf-8",
|
|
28
|
+
env_nested_delimiter="__",
|
|
29
|
+
|
|
30
|
+
yaml_file=get_yaml_config_file_or_default(),
|
|
31
|
+
yaml_file_encoding="utf-8",
|
|
32
|
+
|
|
33
|
+
json_file=get_json_config_file_or_default(),
|
|
34
|
+
json_file_encoding="utf-8"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
llm: LLMConfig
|
|
38
|
+
vcs: VCSConfig
|
|
39
|
+
prompt: PromptConfig = PromptConfig()
|
|
40
|
+
review: ReviewConfig = ReviewConfig()
|
|
41
|
+
logger: LoggerConfig = LoggerConfig()
|
|
42
|
+
artifacts: ArtifactsConfig = ArtifactsConfig()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def settings_customise_sources(
|
|
46
|
+
cls,
|
|
47
|
+
settings_cls: type[BaseSettings],
|
|
48
|
+
init_settings: PydanticBaseSettingsSource,
|
|
49
|
+
env_settings: PydanticBaseSettingsSource,
|
|
50
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
51
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
52
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
53
|
+
return (
|
|
54
|
+
YamlConfigSettingsSource(cls),
|
|
55
|
+
JsonConfigSettingsSource(cls),
|
|
56
|
+
env_settings,
|
|
57
|
+
dotenv_settings,
|
|
58
|
+
init_settings,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
settings = Settings()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Awaitable, Iterable, TypeVar
|
|
3
|
+
|
|
4
|
+
T = TypeVar("T")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def bounded_gather(coroutines: Iterable[Awaitable[T]], concurrency: int = 32) -> tuple[T, ...]:
|
|
8
|
+
sem = asyncio.Semaphore(concurrency)
|
|
9
|
+
|
|
10
|
+
async def wrap(coro: Awaitable[T]) -> T:
|
|
11
|
+
async with sem:
|
|
12
|
+
return await coro
|
|
13
|
+
|
|
14
|
+
return await asyncio.gather(*(wrap(coroutine) for coroutine in coroutines), return_exceptions=True)
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pydantic import BaseModel, DirectoryPath, field_validator, Field
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ArtifactsConfig(BaseModel):
|
|
5
|
+
llm_dir: DirectoryPath = Field(default=DirectoryPath("./artifacts/llm"), validate_default=True)
|
|
6
|
+
llm_enabled: bool = False
|
|
7
|
+
|
|
8
|
+
@field_validator('llm_dir', mode='before')
|
|
9
|
+
def validate_directories(cls, value: DirectoryPath | str) -> DirectoryPath:
|
|
10
|
+
directory = DirectoryPath(value)
|
|
11
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
return directory
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfigEnv(StrEnum):
|
|
6
|
+
ENV = "AI_REVIEW_CONFIG_FILE_ENV"
|
|
7
|
+
YAML = "AI_REVIEW_CONFIG_FILE_YAML"
|
|
8
|
+
JSON = "AI_REVIEW_CONFIG_FILE_JSON"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_config_file_or_default(variable: str, default_filename: str) -> str:
|
|
12
|
+
return os.getenv(variable, os.path.join(os.getcwd(), default_filename))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_env_config_file_or_default() -> str:
|
|
16
|
+
return get_config_file_or_default(ConfigEnv.ENV, ".env")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_yaml_config_file_or_default() -> str:
|
|
20
|
+
return get_config_file_or_default(ConfigEnv.YAML, ".ai-review.yaml")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_json_config_file_or_default() -> str:
|
|
24
|
+
return get_config_file_or_default(ConfigEnv.JSON, ".ai-review.json")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from ai_review.libs.config.http import HTTPClientConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ClaudeMetaConfig(BaseModel):
|
|
7
|
+
model: str = "claude-3-sonnet"
|
|
8
|
+
max_tokens: int = 1200
|
|
9
|
+
temperature: float = 0.3
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClaudeHTTPClientConfig(HTTPClientConfig):
|
|
13
|
+
api_version: str = "2023-06-01"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from ai_review.libs.config.http import HTTPClientConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GeminiMetaConfig(BaseModel):
|
|
7
|
+
model: str = "gemini-2.0-pro"
|
|
8
|
+
max_tokens: int = 1200
|
|
9
|
+
temperature: float = 0.3
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GeminiHTTPClientConfig(HTTPClientConfig):
|
|
13
|
+
pass
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pydantic import BaseModel, HttpUrl, SecretStr
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HTTPClientConfig(BaseModel):
|
|
5
|
+
timeout: float = 120
|
|
6
|
+
api_url: HttpUrl
|
|
7
|
+
api_token: SecretStr
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def api_key(self) -> str:
|
|
11
|
+
return self.api_token.get_secret_value()
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def base_url(self) -> str:
|
|
15
|
+
return str(self.api_url)
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def bearer_token(self) -> str:
|
|
19
|
+
return self.api_token.get_secret_value()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from typing import Annotated, Literal
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from pydantic import BaseModel, Field, FilePath
|
|
6
|
+
|
|
7
|
+
from ai_review.libs.config.claude import ClaudeHTTPClientConfig, ClaudeMetaConfig
|
|
8
|
+
from ai_review.libs.config.gemini import GeminiHTTPClientConfig, GeminiMetaConfig
|
|
9
|
+
from ai_review.libs.config.openai import OpenAIHTTPClientConfig, OpenAIMetaConfig
|
|
10
|
+
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
11
|
+
from ai_review.libs.resources import load_resource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LLMPricingConfig(BaseModel):
|
|
15
|
+
input: float
|
|
16
|
+
output: float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LLMConfigBase(BaseModel):
|
|
20
|
+
provider: LLMProvider
|
|
21
|
+
pricing_file: FilePath | None = None
|
|
22
|
+
|
|
23
|
+
@cached_property
|
|
24
|
+
def pricing_file_or_default(self):
|
|
25
|
+
if self.pricing_file and self.pricing_file.exists():
|
|
26
|
+
return self.pricing_file
|
|
27
|
+
|
|
28
|
+
return load_resource(
|
|
29
|
+
package="ai_review.resources",
|
|
30
|
+
filename="pricing.yaml",
|
|
31
|
+
fallback="ai_review/resources/pricing.yaml"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def load_pricing(self) -> dict[str, LLMPricingConfig]:
|
|
35
|
+
data = self.pricing_file_or_default.read_text(encoding="utf-8")
|
|
36
|
+
raw = yaml.safe_load(data)
|
|
37
|
+
return {model: LLMPricingConfig(**values) for model, values in raw.items()}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenAILLMConfig(LLMConfigBase):
|
|
41
|
+
meta: OpenAIMetaConfig
|
|
42
|
+
provider: Literal[LLMProvider.OPENAI]
|
|
43
|
+
http_client: OpenAIHTTPClientConfig
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GeminiLLMConfig(LLMConfigBase):
|
|
47
|
+
meta: GeminiMetaConfig
|
|
48
|
+
provider: Literal[LLMProvider.GEMINI]
|
|
49
|
+
http_client: GeminiHTTPClientConfig
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ClaudeLLMConfig(LLMConfigBase):
|
|
53
|
+
meta: ClaudeMetaConfig
|
|
54
|
+
provider: Literal[LLMProvider.CLAUDE]
|
|
55
|
+
http_client: ClaudeHTTPClientConfig
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
LLMConfig = Annotated[
|
|
59
|
+
OpenAILLMConfig | GeminiLLMConfig | ClaudeLLMConfig,
|
|
60
|
+
Field(discriminator="provider")
|
|
61
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LoggerLevel(StrEnum):
|
|
7
|
+
NOTSET = "NOTSET"
|
|
8
|
+
DEBUG = "DEBUG"
|
|
9
|
+
INFO = "INFO"
|
|
10
|
+
WARNING = "WARNING"
|
|
11
|
+
ERROR = "ERROR"
|
|
12
|
+
CRITICAL = "CRITICAL"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LoggerConfig(BaseModel):
|
|
16
|
+
level: LoggerLevel = LoggerLevel.INFO
|
|
17
|
+
format: str = "{time:YYYY-MM-DD HH:mm:ss} | {level} | {extra[logger_name]} | {message}"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from ai_review.libs.config.http import HTTPClientConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OpenAIMetaConfig(BaseModel):
|
|
7
|
+
model: str = "gpt-4o-mini"
|
|
8
|
+
max_tokens: int = 1200
|
|
9
|
+
temperature: float = 0.3
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenAIHTTPClientConfig(HTTPClientConfig):
|
|
13
|
+
pass
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, FilePath, Field
|
|
5
|
+
|
|
6
|
+
from ai_review.libs.resources import load_resource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PromptConfig(BaseModel):
|
|
10
|
+
context: dict[str, str] = Field(default_factory=dict)
|
|
11
|
+
inline_prompt_files: list[FilePath] | None = None
|
|
12
|
+
context_prompt_files: list[FilePath] | None = None
|
|
13
|
+
summary_prompt_files: list[FilePath] | None = None
|
|
14
|
+
system_inline_prompt_files: list[FilePath] | None = None
|
|
15
|
+
system_context_prompt_files: list[FilePath] | None = None
|
|
16
|
+
system_summary_prompt_files: list[FilePath] | None = None
|
|
17
|
+
include_inline_system_prompts: bool = True
|
|
18
|
+
include_context_system_prompts: bool = True
|
|
19
|
+
include_summary_system_prompts: bool = True
|
|
20
|
+
|
|
21
|
+
@cached_property
|
|
22
|
+
def inline_prompt_files_or_default(self) -> list[Path]:
|
|
23
|
+
return self.inline_prompt_files or [
|
|
24
|
+
load_resource(
|
|
25
|
+
package="ai_review.prompts",
|
|
26
|
+
filename="default_inline.md",
|
|
27
|
+
fallback="ai_review/prompts/default_inline.md"
|
|
28
|
+
)
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
@cached_property
|
|
32
|
+
def context_prompt_files_or_default(self) -> list[Path]:
|
|
33
|
+
return self.context_prompt_files or [
|
|
34
|
+
load_resource(
|
|
35
|
+
package="ai_review.prompts",
|
|
36
|
+
filename="default_context.md",
|
|
37
|
+
fallback="ai_review/prompts/default_context.md"
|
|
38
|
+
)
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
@cached_property
|
|
42
|
+
def summary_prompt_files_or_default(self) -> list[Path]:
|
|
43
|
+
return self.summary_prompt_files or [
|
|
44
|
+
load_resource(
|
|
45
|
+
package="ai_review.prompts",
|
|
46
|
+
filename="default_summary.md",
|
|
47
|
+
fallback="ai_review/prompts/default_summary.md"
|
|
48
|
+
)
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
@cached_property
|
|
52
|
+
def system_inline_prompt_files_or_default(self) -> list[Path]:
|
|
53
|
+
global_files = [
|
|
54
|
+
load_resource(
|
|
55
|
+
package="ai_review.prompts",
|
|
56
|
+
filename="default_system_inline.md",
|
|
57
|
+
fallback="ai_review/prompts/default_system_inline.md"
|
|
58
|
+
)
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
if self.system_inline_prompt_files is None:
|
|
62
|
+
return global_files
|
|
63
|
+
|
|
64
|
+
if self.include_inline_system_prompts:
|
|
65
|
+
return global_files + self.system_inline_prompt_files
|
|
66
|
+
|
|
67
|
+
return self.system_inline_prompt_files
|
|
68
|
+
|
|
69
|
+
@cached_property
|
|
70
|
+
def system_context_prompt_files_or_default(self) -> list[Path]:
|
|
71
|
+
global_files = [
|
|
72
|
+
load_resource(
|
|
73
|
+
package="ai_review.prompts",
|
|
74
|
+
filename="default_system_context.md",
|
|
75
|
+
fallback="ai_review/prompts/default_system_context.md"
|
|
76
|
+
)
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
if self.system_context_prompt_files is None:
|
|
80
|
+
return global_files
|
|
81
|
+
|
|
82
|
+
if self.include_context_system_prompts:
|
|
83
|
+
return global_files + self.system_context_prompt_files
|
|
84
|
+
|
|
85
|
+
return self.system_context_prompt_files
|
|
86
|
+
|
|
87
|
+
@cached_property
|
|
88
|
+
def system_summary_prompt_files_or_default(self) -> list[Path]:
|
|
89
|
+
global_files = [
|
|
90
|
+
load_resource(
|
|
91
|
+
package="ai_review.prompts",
|
|
92
|
+
filename="default_system_summary.md",
|
|
93
|
+
fallback="ai_review/prompts/default_system_summary.md"
|
|
94
|
+
)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
if self.system_summary_prompt_files is None:
|
|
98
|
+
return global_files
|
|
99
|
+
|
|
100
|
+
if self.include_summary_system_prompts:
|
|
101
|
+
return global_files + self.system_summary_prompt_files
|
|
102
|
+
|
|
103
|
+
return self.system_summary_prompt_files
|
|
104
|
+
|
|
105
|
+
def load_inline(self) -> list[str]:
|
|
106
|
+
return [file.read_text(encoding="utf-8") for file in self.inline_prompt_files_or_default]
|
|
107
|
+
|
|
108
|
+
def load_context(self) -> list[str]:
|
|
109
|
+
return [file.read_text(encoding="utf-8") for file in self.context_prompt_files_or_default]
|
|
110
|
+
|
|
111
|
+
def load_summary(self) -> list[str]:
|
|
112
|
+
return [file.read_text(encoding="utf-8") for file in self.summary_prompt_files_or_default]
|
|
113
|
+
|
|
114
|
+
def load_system_inline(self) -> list[str]:
|
|
115
|
+
return [file.read_text(encoding="utf-8") for file in self.system_inline_prompt_files_or_default]
|
|
116
|
+
|
|
117
|
+
def load_system_context(self) -> list[str]:
|
|
118
|
+
return [file.read_text(encoding="utf-8") for file in self.system_context_prompt_files_or_default]
|
|
119
|
+
|
|
120
|
+
def load_system_summary(self) -> list[str]:
|
|
121
|
+
return [file.read_text(encoding="utf-8") for file in self.system_summary_prompt_files_or_default]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ReviewMode(StrEnum):
|
|
7
|
+
FULL_FILE_DIFF = "FULL_FILE_DIFF"
|
|
8
|
+
FULL_FILE_CURRENT = "FULL_FILE_CURRENT"
|
|
9
|
+
FULL_FILE_PREVIOUS = "FULL_FILE_PREVIOUS"
|
|
10
|
+
|
|
11
|
+
ONLY_ADDED = "ONLY_ADDED"
|
|
12
|
+
ONLY_REMOVED = "ONLY_REMOVED"
|
|
13
|
+
ADDED_AND_REMOVED = "ADDED_AND_REMOVED"
|
|
14
|
+
|
|
15
|
+
ONLY_ADDED_WITH_CONTEXT = "ONLY_ADDED_WITH_CONTEXT"
|
|
16
|
+
ONLY_REMOVED_WITH_CONTEXT = "ONLY_REMOVED_WITH_CONTEXT"
|
|
17
|
+
ADDED_AND_REMOVED_WITH_CONTEXT = "ADDED_AND_REMOVED_WITH_CONTEXT"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ReviewConfig(BaseModel):
|
|
21
|
+
mode: ReviewMode = ReviewMode.FULL_FILE_DIFF
|
|
22
|
+
inline_tag: str = Field(default="#ai-review-inline")
|
|
23
|
+
summary_tag: str = Field(default="#ai-review-summary")
|
|
24
|
+
context_lines: int = Field(default=10, ge=0)
|
|
25
|
+
allow_changes: list[str] = Field(default_factory=list)
|
|
26
|
+
ignore_changes: list[str] = Field(default_factory=list)
|
|
27
|
+
review_added_marker: str = " # added"
|
|
28
|
+
review_removed_marker: str = " # removed"
|
|
29
|
+
max_inline_comments: int | None = None
|
|
30
|
+
max_context_comments: int | None = None
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Annotated, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from ai_review.libs.config.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
|
|
6
|
+
from ai_review.libs.constants.vcs_provider import VCSProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VCSConfigBase(BaseModel):
|
|
10
|
+
provider: VCSProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GitLabVCSConfig(VCSConfigBase):
|
|
14
|
+
provider: Literal[VCSProvider.GITLAB]
|
|
15
|
+
pipeline: GitLabPipelineConfig
|
|
16
|
+
http_client: GitLabHTTPClientConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
VCSConfig = Annotated[GitLabVCSConfig, Field(discriminator="provider")]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FileMode(Enum):
|
|
7
|
+
DELETED = auto()
|
|
8
|
+
MODIFIED = auto()
|
|
9
|
+
NEW = auto()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DiffLineType(Enum):
|
|
13
|
+
ADDED = auto()
|
|
14
|
+
REMOVED = auto()
|
|
15
|
+
UNCHANGED = auto()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class DiffLine:
|
|
20
|
+
type: DiffLineType
|
|
21
|
+
number: int | None
|
|
22
|
+
content: str
|
|
23
|
+
position: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class DiffRange:
|
|
28
|
+
start: int
|
|
29
|
+
length: int
|
|
30
|
+
lines: List[DiffLine]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DiffHunk:
|
|
35
|
+
header: str
|
|
36
|
+
orig_range: DiffRange
|
|
37
|
+
new_range: DiffRange
|
|
38
|
+
lines: List[DiffLine]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class DiffFile:
|
|
43
|
+
header: str
|
|
44
|
+
mode: FileMode
|
|
45
|
+
orig_name: str
|
|
46
|
+
new_name: str
|
|
47
|
+
hunks: List[DiffHunk]
|
|
48
|
+
|
|
49
|
+
def added_new_lines(self) -> list[DiffLine]:
|
|
50
|
+
return [
|
|
51
|
+
line
|
|
52
|
+
for hunk in self.hunks
|
|
53
|
+
for line in hunk.new_range.lines
|
|
54
|
+
if line.type is DiffLineType.ADDED
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
def removed_old_lines(self) -> list[DiffLine]:
|
|
58
|
+
return [
|
|
59
|
+
line
|
|
60
|
+
for hunk in self.hunks
|
|
61
|
+
for line in hunk.orig_range.lines
|
|
62
|
+
if line.type is DiffLineType.REMOVED
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def added_line_numbers(self) -> set[int]:
|
|
66
|
+
return {line.number for line in self.added_new_lines() if line.number is not None}
|
|
67
|
+
|
|
68
|
+
def removed_line_numbers(self) -> set[int]:
|
|
69
|
+
return {line.number for line in self.removed_old_lines() if line.number is not None}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Diff:
|
|
74
|
+
files: List[DiffFile]
|
|
75
|
+
raw: str
|
|
76
|
+
|
|
77
|
+
def summary(self) -> str:
|
|
78
|
+
parts = []
|
|
79
|
+
for file in self.files:
|
|
80
|
+
parts.append(f"{file.mode.name} {file.new_name or file.orig_name}")
|
|
81
|
+
for hunk in file.hunks:
|
|
82
|
+
parts.append(f" Hunk: {hunk.header} ({len(hunk.lines)} lines)")
|
|
83
|
+
|
|
84
|
+
return "\n".join(parts)
|
|
85
|
+
|
|
86
|
+
def changed_lines(self) -> dict[str, list[int]]:
|
|
87
|
+
result: dict[str, list[int]] = {}
|
|
88
|
+
for file in self.files:
|
|
89
|
+
if file.mode == FileMode.DELETED:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
result[file.new_name] = [
|
|
93
|
+
line.number for h in file.hunks for line in h.new_range.lines
|
|
94
|
+
if line.type == DiffLineType.ADDED
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
def changed_files(self) -> list[str]:
|
|
100
|
+
return [file.new_name for file in self.files if file.mode != FileMode.DELETED]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from ai_review.libs.diff.models import (
|
|
4
|
+
Diff,
|
|
5
|
+
DiffFile,
|
|
6
|
+
DiffHunk,
|
|
7
|
+
DiffLine,
|
|
8
|
+
DiffLineType,
|
|
9
|
+
DiffRange,
|
|
10
|
+
FileMode,
|
|
11
|
+
)
|
|
12
|
+
from ai_review.libs.diff.tools import is_source_line, get_line_type
|
|
13
|
+
|
|
14
|
+
HUNK_RE = re.compile(r"@@ -(\d+),?(\d+)? \+(\d+),?(\d+)? @@ ?(.*)?")
|
|
15
|
+
OLD_FILE_PREFIX = "--- a/"
|
|
16
|
+
NEW_FILE_PREFIX = "+++ b/"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DiffParser:
|
|
20
|
+
@classmethod
|
|
21
|
+
def parse(cls, diff_string: str) -> Diff:
|
|
22
|
+
lines = diff_string.splitlines()
|
|
23
|
+
files: list[DiffFile] = []
|
|
24
|
+
|
|
25
|
+
current_file: DiffFile | None = None
|
|
26
|
+
current_hunk: DiffHunk | None = None
|
|
27
|
+
|
|
28
|
+
added_count = removed_count = 0
|
|
29
|
+
diff_pos = 0
|
|
30
|
+
|
|
31
|
+
for raw in lines:
|
|
32
|
+
diff_pos += 1
|
|
33
|
+
|
|
34
|
+
# Начало нового файла
|
|
35
|
+
if raw.startswith("diff "):
|
|
36
|
+
current_file = DiffFile(
|
|
37
|
+
header=raw,
|
|
38
|
+
mode=FileMode.MODIFIED,
|
|
39
|
+
orig_name="",
|
|
40
|
+
new_name="",
|
|
41
|
+
hunks=[],
|
|
42
|
+
)
|
|
43
|
+
files.append(current_file)
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
# Дополняем header файла
|
|
47
|
+
if raw.startswith("index ") or raw.startswith("--- ") or raw.startswith("+++ "):
|
|
48
|
+
current_file.header += "\n" + raw
|
|
49
|
+
|
|
50
|
+
if raw.startswith(OLD_FILE_PREFIX):
|
|
51
|
+
current_file.orig_name = raw[len(OLD_FILE_PREFIX):]
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if raw.startswith(NEW_FILE_PREFIX):
|
|
55
|
+
current_file.new_name = raw[len(NEW_FILE_PREFIX):]
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
if raw == "+++ /dev/null":
|
|
59
|
+
current_file.mode = FileMode.DELETED
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if raw == "--- /dev/null":
|
|
63
|
+
current_file.mode = FileMode.NEW
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if raw.startswith("@@ "):
|
|
67
|
+
match = HUNK_RE.match(raw)
|
|
68
|
+
if not match:
|
|
69
|
+
raise ValueError(f"Invalid hunk header: {raw}")
|
|
70
|
+
|
|
71
|
+
a, b, c, d, header = match.groups()
|
|
72
|
+
orig_start, orig_len = int(a), int(b or 0)
|
|
73
|
+
new_start, new_len = int(c), int(d or 0)
|
|
74
|
+
|
|
75
|
+
current_hunk = DiffHunk(
|
|
76
|
+
header=header or "",
|
|
77
|
+
orig_range=DiffRange(orig_start, orig_len, []),
|
|
78
|
+
new_range=DiffRange(new_start, new_len, []),
|
|
79
|
+
lines=[],
|
|
80
|
+
)
|
|
81
|
+
current_file.hunks.append(current_hunk)
|
|
82
|
+
|
|
83
|
+
added_count, removed_count = new_start, orig_start
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if current_hunk and is_source_line(raw):
|
|
87
|
+
line_type = get_line_type(raw)
|
|
88
|
+
content = raw[1:]
|
|
89
|
+
|
|
90
|
+
if line_type is DiffLineType.ADDED:
|
|
91
|
+
line = DiffLine(line_type, added_count, content, diff_pos)
|
|
92
|
+
current_hunk.new_range.lines.append(line)
|
|
93
|
+
current_hunk.lines.append(line)
|
|
94
|
+
added_count += 1
|
|
95
|
+
|
|
96
|
+
elif line_type is DiffLineType.REMOVED:
|
|
97
|
+
line = DiffLine(line_type, removed_count, content, diff_pos)
|
|
98
|
+
current_hunk.orig_range.lines.append(line)
|
|
99
|
+
current_hunk.lines.append(line)
|
|
100
|
+
removed_count += 1
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
line_new = DiffLine(DiffLineType.UNCHANGED, added_count, content, diff_pos)
|
|
104
|
+
line_old = DiffLine(DiffLineType.UNCHANGED, removed_count, content, diff_pos)
|
|
105
|
+
current_hunk.new_range.lines.append(line_new)
|
|
106
|
+
current_hunk.orig_range.lines.append(line_old)
|
|
107
|
+
current_hunk.lines.append(line_new)
|
|
108
|
+
added_count += 1
|
|
109
|
+
removed_count += 1
|
|
110
|
+
|
|
111
|
+
return Diff(files=files, raw=diff_string)
|