codeyak 0.0.8__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.
- codeyak/__init__.py +8 -0
- codeyak/__main__.py +27 -0
- codeyak/apps/__init__.py +3 -0
- codeyak/apps/cli/__init__.py +7 -0
- codeyak/apps/cli/configure.py +199 -0
- codeyak/apps/cli/main.py +362 -0
- codeyak/config.py +145 -0
- codeyak/domain/__init__.py +52 -0
- codeyak/domain/constants.py +41 -0
- codeyak/domain/exceptions.py +55 -0
- codeyak/domain/models.py +411 -0
- codeyak/infrastructure/__init__.py +17 -0
- codeyak/infrastructure/llm/azure.py +59 -0
- codeyak/infrastructure/vcs/diff_parser.py +110 -0
- codeyak/infrastructure/vcs/gitlab.py +342 -0
- codeyak/infrastructure/vcs/local_git.py +322 -0
- codeyak/prebuilt/__init__.py +0 -0
- codeyak/prebuilt/code-quality.yaml +32 -0
- codeyak/prebuilt/default.yaml +6 -0
- codeyak/prebuilt/security.yaml +51 -0
- codeyak/protocols/__init__.py +184 -0
- codeyak/py.typed +0 -0
- codeyak/services/__init__.py +23 -0
- codeyak/services/code.py +92 -0
- codeyak/services/context/__init__.py +26 -0
- codeyak/services/context/planner.py +192 -0
- codeyak/services/context/renderer.py +267 -0
- codeyak/services/context/skeleton.py +445 -0
- codeyak/services/context/symbol_index.py +820 -0
- codeyak/services/context_builder.py +293 -0
- codeyak/services/feedback/__init__.py +11 -0
- codeyak/services/feedback/console.py +86 -0
- codeyak/services/feedback/merge_request.py +90 -0
- codeyak/services/guidelines/__init__.py +30 -0
- codeyak/services/guidelines/generator.py +494 -0
- codeyak/services/guidelines/parser.py +461 -0
- codeyak/services/guidelines/provider.py +376 -0
- codeyak/services/reviewer.py +341 -0
- codeyak/services/summary.py +131 -0
- codeyak/ui/__init__.py +10 -0
- codeyak/ui/console.py +25 -0
- codeyak/ui/progress.py +268 -0
- codeyak-0.0.8.dist-info/METADATA +136 -0
- codeyak-0.0.8.dist-info/RECORD +47 -0
- codeyak-0.0.8.dist-info/WHEEL +4 -0
- codeyak-0.0.8.dist-info/entry_points.txt +2 -0
- codeyak-0.0.8.dist-info/licenses/LICENSE +21 -0
codeyak/__init__.py
ADDED
codeyak/__main__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for running codeyak as a module.
|
|
3
|
+
|
|
4
|
+
Supports both the new CLI interface and backwards-compatible direct invocation:
|
|
5
|
+
python -m codeyak review # New: local review
|
|
6
|
+
python -m codeyak mr <MR_ID> [PROJECT_ID] # New: MR review
|
|
7
|
+
python -m codeyak <MR_ID> [PROJECT_ID] # Legacy: MR review
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""Entry point that handles both CLI and legacy invocation."""
|
|
15
|
+
# Check if using legacy invocation (first arg is a number = MR_ID)
|
|
16
|
+
if len(sys.argv) > 1 and sys.argv[1].isdigit():
|
|
17
|
+
# Legacy mode: convert to new CLI format
|
|
18
|
+
# python -m codeyak <MR_ID> [PROJECT_ID] -> yak mr <MR_ID> [PROJECT_ID]
|
|
19
|
+
sys.argv = [sys.argv[0], "mr"] + sys.argv[1:]
|
|
20
|
+
|
|
21
|
+
# Import and run CLI
|
|
22
|
+
from .apps.cli import main as cli_main
|
|
23
|
+
cli_main()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
main()
|
codeyak/apps/__init__.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive init flow for CodeYak configuration.
|
|
3
|
+
|
|
4
|
+
This module provides functions to interactively configure CodeYak settings
|
|
5
|
+
when users run commands without having configured the tool first.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import tomllib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import tomli_w
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
|
|
16
|
+
from ...config import get_config_path, reset_settings
|
|
17
|
+
from ...ui import console, BRAND_BORDER
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _show_key_feedback(key: str, label: str) -> None:
|
|
21
|
+
"""Show feedback about entered key without revealing it."""
|
|
22
|
+
if not key:
|
|
23
|
+
console.print(f" [warning]Warning: No {label} was entered[/warning]")
|
|
24
|
+
elif len(key) < 10:
|
|
25
|
+
console.print(f" [success]{label} entered[/success] [muted]({len(key)} characters)[/muted]")
|
|
26
|
+
else:
|
|
27
|
+
# Show first 4 and last 4 chars for verification
|
|
28
|
+
masked = f"{key[:4]}...{key[-4:]}"
|
|
29
|
+
console.print(f" [success]{label} entered:[/success] {masked} [muted]({len(key)} characters)[/muted]")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_existing_config() -> dict:
|
|
33
|
+
"""Load existing config if it exists, otherwise return empty dict."""
|
|
34
|
+
config_path = get_config_path()
|
|
35
|
+
if config_path.exists():
|
|
36
|
+
with open(config_path, "rb") as f:
|
|
37
|
+
return tomllib.load(f)
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save_config(config: dict) -> None:
|
|
42
|
+
"""Save config to TOML file with restrictive permissions."""
|
|
43
|
+
config_path = get_config_path()
|
|
44
|
+
|
|
45
|
+
# Create parent directories if needed
|
|
46
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# Write config with restrictive permissions (owner read/write only)
|
|
49
|
+
with open(config_path, "wb") as f:
|
|
50
|
+
tomli_w.dump(config, f)
|
|
51
|
+
|
|
52
|
+
# Set file permissions to 600 (owner read/write only)
|
|
53
|
+
os.chmod(config_path, 0o600)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_llm_init() -> None:
|
|
57
|
+
"""Run interactive init flow for LLM (Azure OpenAI) configuration only."""
|
|
58
|
+
console.print()
|
|
59
|
+
console.print(Panel(
|
|
60
|
+
"[brand]LLM Provider[/brand]",
|
|
61
|
+
border_style=BRAND_BORDER,
|
|
62
|
+
padding=(0, 2)
|
|
63
|
+
))
|
|
64
|
+
console.print("Available providers:")
|
|
65
|
+
console.print(" 1. Azure OpenAI")
|
|
66
|
+
console.print()
|
|
67
|
+
|
|
68
|
+
# Azure OpenAI Endpoint
|
|
69
|
+
console.print(" [muted]Example: https://your-resource.openai.azure.com/[/muted]")
|
|
70
|
+
endpoint = click.prompt(" Azure OpenAI Endpoint", type=str)
|
|
71
|
+
|
|
72
|
+
# API Key
|
|
73
|
+
console.print()
|
|
74
|
+
console.print(" [muted]Found in Azure Portal > Your OpenAI Resource > Keys and Endpoint[/muted]")
|
|
75
|
+
api_key = click.prompt(" Azure OpenAI API Key", type=str, hide_input=True)
|
|
76
|
+
_show_key_feedback(api_key, "API Key")
|
|
77
|
+
|
|
78
|
+
# Deployment Name
|
|
79
|
+
console.print()
|
|
80
|
+
deployment_name = click.prompt(
|
|
81
|
+
" Deployment Name", type=str, default="gpt-4o", show_default=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# API Version
|
|
85
|
+
api_version = click.prompt(
|
|
86
|
+
" API Version", type=str, default="2024-02-15-preview", show_default=True
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Load existing config and update with new values
|
|
90
|
+
config = _load_existing_config()
|
|
91
|
+
config["AZURE_OPENAI_ENDPOINT"] = endpoint
|
|
92
|
+
config["AZURE_OPENAI_API_KEY"] = api_key
|
|
93
|
+
config["AZURE_DEPLOYMENT_NAME"] = deployment_name
|
|
94
|
+
config["AZURE_OPENAI_API_VERSION"] = api_version
|
|
95
|
+
|
|
96
|
+
_save_config(config)
|
|
97
|
+
reset_settings()
|
|
98
|
+
|
|
99
|
+
config_path = get_config_path()
|
|
100
|
+
console.print()
|
|
101
|
+
console.print(f"[success]Configuration saved to {config_path}[/success]")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_gitlab_init() -> None:
|
|
105
|
+
"""Run interactive init flow for GitLab configuration only."""
|
|
106
|
+
console.print()
|
|
107
|
+
console.print(Panel(
|
|
108
|
+
"[brand]GitLab Configuration[/brand]",
|
|
109
|
+
border_style=BRAND_BORDER,
|
|
110
|
+
padding=(0, 2)
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
# GitLab URL
|
|
114
|
+
gitlab_url = click.prompt(
|
|
115
|
+
" GitLab URL", type=str, default="https://gitlab.com", show_default=True
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# GitLab Token
|
|
119
|
+
console.print()
|
|
120
|
+
console.print(" [muted]Personal Access Token (create at GitLab > Settings > Access Tokens)[/muted]")
|
|
121
|
+
gitlab_token = click.prompt(" GitLab Token", type=str, hide_input=True)
|
|
122
|
+
_show_key_feedback(gitlab_token, "Token")
|
|
123
|
+
|
|
124
|
+
# Load existing config and update with new values
|
|
125
|
+
config = _load_existing_config()
|
|
126
|
+
config["GITLAB_URL"] = gitlab_url
|
|
127
|
+
config["GITLAB_TOKEN"] = gitlab_token
|
|
128
|
+
|
|
129
|
+
_save_config(config)
|
|
130
|
+
reset_settings()
|
|
131
|
+
|
|
132
|
+
console.print()
|
|
133
|
+
console.print(f"[success]Configuration saved to {get_config_path()}[/success]")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def run_langfuse_init() -> None:
|
|
137
|
+
"""Run interactive init flow for Langfuse configuration only."""
|
|
138
|
+
console.print()
|
|
139
|
+
console.print(Panel(
|
|
140
|
+
"[brand]Langfuse Configuration (Optional)[/brand]",
|
|
141
|
+
border_style=BRAND_BORDER,
|
|
142
|
+
padding=(0, 2)
|
|
143
|
+
))
|
|
144
|
+
console.print(" [muted]Langfuse provides observability for your LLM calls.[/muted]")
|
|
145
|
+
console.print()
|
|
146
|
+
|
|
147
|
+
# Secret Key
|
|
148
|
+
secret_key = click.prompt(" Langfuse Secret Key", type=str, hide_input=True)
|
|
149
|
+
_show_key_feedback(secret_key, "Secret Key")
|
|
150
|
+
|
|
151
|
+
# Public Key
|
|
152
|
+
public_key = click.prompt(" Langfuse Public Key", type=str)
|
|
153
|
+
|
|
154
|
+
# Host
|
|
155
|
+
host = click.prompt(
|
|
156
|
+
" Langfuse Host",
|
|
157
|
+
type=str,
|
|
158
|
+
default="https://cloud.langfuse.com",
|
|
159
|
+
show_default=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Load existing config and update with new values
|
|
163
|
+
config = _load_existing_config()
|
|
164
|
+
config["LANGFUSE_SECRET_KEY"] = secret_key
|
|
165
|
+
config["LANGFUSE_PUBLIC_KEY"] = public_key
|
|
166
|
+
config["LANGFUSE_HOST"] = host
|
|
167
|
+
|
|
168
|
+
_save_config(config)
|
|
169
|
+
reset_settings()
|
|
170
|
+
|
|
171
|
+
console.print()
|
|
172
|
+
console.print(f"[success]Configuration saved to {get_config_path()}[/success]")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def run_full_init(include_gitlab: bool = False) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Run the complete first-time setup flow.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
include_gitlab: If True, also prompt for GitLab configuration.
|
|
181
|
+
"""
|
|
182
|
+
console.print()
|
|
183
|
+
console.print("[info]Looks like you haven't configured CodeYak yet. Let's get you set up![/info]")
|
|
184
|
+
|
|
185
|
+
# Always configure LLM
|
|
186
|
+
run_llm_init()
|
|
187
|
+
|
|
188
|
+
# Optionally configure GitLab
|
|
189
|
+
if include_gitlab:
|
|
190
|
+
run_gitlab_init()
|
|
191
|
+
|
|
192
|
+
# Ask about Langfuse
|
|
193
|
+
console.print()
|
|
194
|
+
if click.confirm("Would you like to configure Langfuse for observability?", default=False):
|
|
195
|
+
run_langfuse_init()
|
|
196
|
+
|
|
197
|
+
console.print()
|
|
198
|
+
console.print("[info]Continuing with your command...[/info]")
|
|
199
|
+
console.print()
|
codeyak/apps/cli/main.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI for CodeYak - Local and MR code review.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
yak review # Review local uncommitted changes
|
|
6
|
+
yak mr <MR_ID> [PROJECT_ID] # Review GitLab MR
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from ... import __version__
|
|
16
|
+
from ...config import (
|
|
17
|
+
get_settings,
|
|
18
|
+
is_gitlab_configured,
|
|
19
|
+
is_llm_configured,
|
|
20
|
+
is_langfuse_configured,
|
|
21
|
+
config_file_exists,
|
|
22
|
+
)
|
|
23
|
+
from .configure import run_full_init, run_gitlab_init, run_llm_init
|
|
24
|
+
from ...infrastructure import GitLabAdapter, LocalGitAdapter, AzureAdapter
|
|
25
|
+
from ...services import (
|
|
26
|
+
CodeReviewer,
|
|
27
|
+
GuidelinesProvider,
|
|
28
|
+
GuidelinesGenerator,
|
|
29
|
+
CodeProvider,
|
|
30
|
+
CodeReviewContextBuilder,
|
|
31
|
+
MergeRequestFeedbackPublisher,
|
|
32
|
+
ConsoleFeedbackPublisher,
|
|
33
|
+
SummaryGenerator,
|
|
34
|
+
)
|
|
35
|
+
from ...ui import console, RichProgressReporter, CIProgressReporter
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def ensure_llm_configured() -> None:
|
|
39
|
+
"""Ensure LLM (Azure OpenAI) is configured. Triggers init if not."""
|
|
40
|
+
if is_llm_configured():
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Check if this is a CI/CD environment (no TTY)
|
|
44
|
+
if not sys.stdin.isatty():
|
|
45
|
+
click.echo(
|
|
46
|
+
"Error: Azure OpenAI is not configured. "
|
|
47
|
+
"Set AZURE_OPENAI_API_KEY and AZURE_OPENAI_ENDPOINT environment variables.",
|
|
48
|
+
err=True,
|
|
49
|
+
)
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
# First time setup - no config file exists
|
|
53
|
+
if not config_file_exists():
|
|
54
|
+
run_full_init(include_gitlab=False)
|
|
55
|
+
else:
|
|
56
|
+
# Config file exists but LLM not configured
|
|
57
|
+
console.print()
|
|
58
|
+
console.print("[info]LLM provider is not configured. Let's set it up![/info]")
|
|
59
|
+
run_llm_init()
|
|
60
|
+
console.print()
|
|
61
|
+
console.print("[info]Continuing with your command...[/info]")
|
|
62
|
+
console.print()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def ensure_gitlab_configured() -> None:
|
|
66
|
+
"""Ensure GitLab is configured. Triggers init if not."""
|
|
67
|
+
# First ensure LLM is configured
|
|
68
|
+
ensure_llm_configured()
|
|
69
|
+
|
|
70
|
+
if is_gitlab_configured():
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Check if this is a CI/CD environment (no TTY)
|
|
74
|
+
if not sys.stdin.isatty():
|
|
75
|
+
click.echo(
|
|
76
|
+
"Error: GitLab is not configured. "
|
|
77
|
+
"Set GITLAB_TOKEN environment variable.",
|
|
78
|
+
err=True,
|
|
79
|
+
)
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
|
|
82
|
+
# First time setup - no config file exists
|
|
83
|
+
if not config_file_exists():
|
|
84
|
+
run_full_init(include_gitlab=True)
|
|
85
|
+
else:
|
|
86
|
+
# Config file exists but GitLab not configured
|
|
87
|
+
console.print()
|
|
88
|
+
console.print("[info]GitLab integration is not configured. Let's set it up![/info]")
|
|
89
|
+
run_gitlab_init()
|
|
90
|
+
console.print()
|
|
91
|
+
console.print("[info]Continuing with your command...[/info]")
|
|
92
|
+
console.print()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@click.group()
|
|
96
|
+
@click.version_option(version=__version__, prog_name="yak")
|
|
97
|
+
def main():
|
|
98
|
+
"""CodeYak - AI-powered code review tool."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@main.command()
|
|
103
|
+
@click.option(
|
|
104
|
+
"--path",
|
|
105
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
106
|
+
default=None,
|
|
107
|
+
help="Path to git repository. Defaults to current directory."
|
|
108
|
+
)
|
|
109
|
+
def review(path: Path | None):
|
|
110
|
+
"""Review local uncommitted changes."""
|
|
111
|
+
# Show banner first
|
|
112
|
+
progress = RichProgressReporter()
|
|
113
|
+
progress.banner("Codeyak", __version__)
|
|
114
|
+
|
|
115
|
+
# Ensure LLM is configured before proceeding
|
|
116
|
+
ensure_llm_configured()
|
|
117
|
+
|
|
118
|
+
repo_path = path or Path.cwd()
|
|
119
|
+
|
|
120
|
+
# Show observability status
|
|
121
|
+
obs_status = "ON" if is_langfuse_configured() else "OFF"
|
|
122
|
+
progress.info(f"Observability: {obs_status}")
|
|
123
|
+
progress.info(f"Reviewing uncommitted changes in {repo_path}...")
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
# Initialize adapters
|
|
127
|
+
vcs = LocalGitAdapter(repo_path)
|
|
128
|
+
llm = AzureAdapter(
|
|
129
|
+
api_key=get_settings().AZURE_OPENAI_API_KEY,
|
|
130
|
+
endpoint=get_settings().AZURE_OPENAI_ENDPOINT,
|
|
131
|
+
deployment_name=get_settings().AZURE_DEPLOYMENT_NAME,
|
|
132
|
+
api_version=get_settings().AZURE_OPENAI_API_VERSION
|
|
133
|
+
)
|
|
134
|
+
except ValueError as e:
|
|
135
|
+
click.echo(f"Error: {e}", err=True)
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
click.echo(f"Error initializing: {e}", err=True)
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
langfuse_enabled = bool(
|
|
142
|
+
get_settings().LANGFUSE_SECRET_KEY and
|
|
143
|
+
get_settings().LANGFUSE_PUBLIC_KEY
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
langfuse = None
|
|
147
|
+
if langfuse_enabled:
|
|
148
|
+
from langfuse import Langfuse
|
|
149
|
+
langfuse = Langfuse(
|
|
150
|
+
secret_key=get_settings().LANGFUSE_SECRET_KEY,
|
|
151
|
+
public_key=get_settings().LANGFUSE_PUBLIC_KEY,
|
|
152
|
+
host=get_settings().LANGFUSE_HOST
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Initialize services - CodeProvider handles all MergeRequest construction
|
|
156
|
+
context = CodeReviewContextBuilder(
|
|
157
|
+
llm_client=llm,
|
|
158
|
+
repo_path=repo_path,
|
|
159
|
+
use_smart_context=True,
|
|
160
|
+
progress=progress,
|
|
161
|
+
)
|
|
162
|
+
guidelines = GuidelinesProvider(vcs)
|
|
163
|
+
code = CodeProvider(vcs)
|
|
164
|
+
feedback = ConsoleFeedbackPublisher()
|
|
165
|
+
summary = SummaryGenerator(llm, langfuse=langfuse)
|
|
166
|
+
|
|
167
|
+
bot = CodeReviewer(
|
|
168
|
+
context=context,
|
|
169
|
+
code=code,
|
|
170
|
+
guidelines=guidelines,
|
|
171
|
+
llm=llm,
|
|
172
|
+
feedback=feedback,
|
|
173
|
+
summary=summary,
|
|
174
|
+
langfuse=langfuse,
|
|
175
|
+
progress=progress,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
bot.review_local_changes()
|
|
179
|
+
|
|
180
|
+
# Flush Langfuse traces
|
|
181
|
+
if langfuse:
|
|
182
|
+
langfuse.flush()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@main.command()
|
|
186
|
+
@click.argument("mr_id")
|
|
187
|
+
@click.argument("project_id", required=False)
|
|
188
|
+
def mr(mr_id: str, project_id: str | None):
|
|
189
|
+
"""Review a GitLab merge request.
|
|
190
|
+
|
|
191
|
+
MR_ID is the merge request ID to review.
|
|
192
|
+
PROJECT_ID is optional (uses CI_PROJECT_ID env var if not provided).
|
|
193
|
+
"""
|
|
194
|
+
# Ensure both LLM and GitLab are configured before proceeding
|
|
195
|
+
ensure_gitlab_configured()
|
|
196
|
+
|
|
197
|
+
# Get project ID from argument or environment
|
|
198
|
+
project_id = project_id or os.getenv("CI_PROJECT_ID")
|
|
199
|
+
|
|
200
|
+
if not project_id:
|
|
201
|
+
click.echo(
|
|
202
|
+
"Error: Project ID is required. "
|
|
203
|
+
"Pass it as the second argument or set CI_PROJECT_ID.",
|
|
204
|
+
err=True
|
|
205
|
+
)
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
|
|
208
|
+
# Show observability status
|
|
209
|
+
obs_status = "[success]ON[/success]" if is_langfuse_configured() else "[muted]OFF[/muted]"
|
|
210
|
+
console.print(f"Observability: {obs_status}")
|
|
211
|
+
console.print(f"[info]Reviewing MR {mr_id} in project {project_id}...[/info]")
|
|
212
|
+
|
|
213
|
+
# Initialize adapters
|
|
214
|
+
try:
|
|
215
|
+
vcs = GitLabAdapter(
|
|
216
|
+
url=get_settings().GITLAB_URL,
|
|
217
|
+
token=get_settings().GITLAB_TOKEN,
|
|
218
|
+
project_id=project_id
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
llm = AzureAdapter(
|
|
222
|
+
api_key=get_settings().AZURE_OPENAI_API_KEY,
|
|
223
|
+
endpoint=get_settings().AZURE_OPENAI_ENDPOINT,
|
|
224
|
+
deployment_name=get_settings().AZURE_DEPLOYMENT_NAME,
|
|
225
|
+
api_version=get_settings().AZURE_OPENAI_API_VERSION
|
|
226
|
+
)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
click.echo(f"Configuration Error: {e}", err=True)
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
|
|
231
|
+
# Initialize Langfuse if configured
|
|
232
|
+
langfuse_enabled = bool(
|
|
233
|
+
get_settings().LANGFUSE_SECRET_KEY and
|
|
234
|
+
get_settings().LANGFUSE_PUBLIC_KEY
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
langfuse = None
|
|
238
|
+
if langfuse_enabled:
|
|
239
|
+
from langfuse import Langfuse
|
|
240
|
+
langfuse = Langfuse(
|
|
241
|
+
secret_key=get_settings().LANGFUSE_SECRET_KEY,
|
|
242
|
+
public_key=get_settings().LANGFUSE_PUBLIC_KEY,
|
|
243
|
+
host=get_settings().LANGFUSE_HOST
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Initialize services
|
|
247
|
+
# Use current directory as repo path - in CI, the repo is checked out
|
|
248
|
+
progress = CIProgressReporter()
|
|
249
|
+
context = CodeReviewContextBuilder(
|
|
250
|
+
llm_client=llm,
|
|
251
|
+
repo_path=Path.cwd(),
|
|
252
|
+
use_smart_context=True,
|
|
253
|
+
progress=progress,
|
|
254
|
+
)
|
|
255
|
+
guidelines = GuidelinesProvider(vcs)
|
|
256
|
+
code = CodeProvider(vcs)
|
|
257
|
+
feedback = MergeRequestFeedbackPublisher(vcs, mr_id)
|
|
258
|
+
summary = SummaryGenerator(llm, langfuse)
|
|
259
|
+
|
|
260
|
+
# Create reviewer and run
|
|
261
|
+
bot = CodeReviewer(
|
|
262
|
+
context=context,
|
|
263
|
+
guidelines=guidelines,
|
|
264
|
+
code=code,
|
|
265
|
+
feedback=feedback,
|
|
266
|
+
llm=llm,
|
|
267
|
+
summary=summary,
|
|
268
|
+
langfuse=langfuse,
|
|
269
|
+
progress=progress,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
bot.review_merge_request(mr_id)
|
|
273
|
+
|
|
274
|
+
# Flush Langfuse traces
|
|
275
|
+
if langfuse:
|
|
276
|
+
langfuse.flush()
|
|
277
|
+
|
|
278
|
+
progress.success("Review complete.")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@main.command()
|
|
282
|
+
@click.option(
|
|
283
|
+
"--days",
|
|
284
|
+
type=int,
|
|
285
|
+
default=365,
|
|
286
|
+
help="Number of days of history to analyze (default: 365)"
|
|
287
|
+
)
|
|
288
|
+
def learn(days: int):
|
|
289
|
+
"""Generate guidelines from git history analysis.
|
|
290
|
+
|
|
291
|
+
Analyzes commits to identify patterns of mistakes and problematic areas,
|
|
292
|
+
then generates codeyak guidelines to help avoid future issues.
|
|
293
|
+
|
|
294
|
+
Output is written to .codeyak/project.yaml
|
|
295
|
+
"""
|
|
296
|
+
# Show banner first
|
|
297
|
+
progress = RichProgressReporter()
|
|
298
|
+
progress.banner("Codeyak", __version__)
|
|
299
|
+
|
|
300
|
+
# Ensure LLM is configured before proceeding
|
|
301
|
+
ensure_llm_configured()
|
|
302
|
+
|
|
303
|
+
repo_path = Path.cwd()
|
|
304
|
+
|
|
305
|
+
# Show observability status
|
|
306
|
+
obs_status = "ON" if is_langfuse_configured() else "OFF"
|
|
307
|
+
progress.info(f"Observability: {obs_status}")
|
|
308
|
+
|
|
309
|
+
# Verify we're in a git repository
|
|
310
|
+
try:
|
|
311
|
+
vcs = LocalGitAdapter(repo_path)
|
|
312
|
+
except ValueError as e:
|
|
313
|
+
click.echo(f"Error: {e}", err=True)
|
|
314
|
+
click.echo("The 'learn' command must be run inside a git repository.", err=True)
|
|
315
|
+
sys.exit(1)
|
|
316
|
+
|
|
317
|
+
progress.info(f"Analyzing git history for the last {days} days...")
|
|
318
|
+
|
|
319
|
+
# Initialize LLM adapter
|
|
320
|
+
try:
|
|
321
|
+
llm = AzureAdapter(
|
|
322
|
+
api_key=get_settings().AZURE_OPENAI_API_KEY,
|
|
323
|
+
endpoint=get_settings().AZURE_OPENAI_ENDPOINT,
|
|
324
|
+
deployment_name=get_settings().AZURE_DEPLOYMENT_NAME,
|
|
325
|
+
api_version=get_settings().AZURE_OPENAI_API_VERSION
|
|
326
|
+
)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
click.echo(f"Error initializing LLM: {e}", err=True)
|
|
329
|
+
sys.exit(1)
|
|
330
|
+
|
|
331
|
+
# Initialize Langfuse if configured
|
|
332
|
+
langfuse = None
|
|
333
|
+
if get_settings().LANGFUSE_SECRET_KEY and get_settings().LANGFUSE_PUBLIC_KEY:
|
|
334
|
+
from langfuse import Langfuse
|
|
335
|
+
langfuse = Langfuse(
|
|
336
|
+
secret_key=get_settings().LANGFUSE_SECRET_KEY,
|
|
337
|
+
public_key=get_settings().LANGFUSE_PUBLIC_KEY,
|
|
338
|
+
host=get_settings().LANGFUSE_HOST
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Generate guidelines
|
|
342
|
+
generator = GuidelinesGenerator(vcs=vcs, llm=llm, langfuse=langfuse, progress=progress)
|
|
343
|
+
yaml_output = generator.generate_from_history(since_days=days)
|
|
344
|
+
|
|
345
|
+
# Create .codeyak/ directory if it doesn't exist
|
|
346
|
+
codeyak_dir = repo_path / ".codeyak"
|
|
347
|
+
codeyak_dir.mkdir(exist_ok=True)
|
|
348
|
+
|
|
349
|
+
# Write output to project.yaml
|
|
350
|
+
output_path = codeyak_dir / "project.yaml"
|
|
351
|
+
output_path.write_text(yaml_output)
|
|
352
|
+
|
|
353
|
+
progress.success(f"Guidelines written to {output_path}")
|
|
354
|
+
progress.info("Review and customize the generated guidelines before using them.")
|
|
355
|
+
|
|
356
|
+
# Flush Langfuse traces
|
|
357
|
+
if langfuse:
|
|
358
|
+
langfuse.flush()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
main()
|