diffrag 0.2.3__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.
diffrag/cli/main.py ADDED
@@ -0,0 +1,440 @@
1
+ """Entry-point for the ``diffrag`` CLI."""
2
+
3
+ import asyncio
4
+ import dataclasses
5
+ import importlib.resources
6
+ import json
7
+ import logging
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ import click
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+
17
+ from ..ai.openai_compat import OpenAICompatClient
18
+ from ..config.settings import Settings, default_db_path, user_config_path
19
+ from ..diff.git import GitRepository
20
+ from ..exceptions import AICodeReviewerError
21
+ from ..indexing.indexer import RepoIndexer
22
+ from ..review.reviewer import CodeReviewer
23
+
24
+ console = Console()
25
+ err_console = Console(stderr=True, style="bold red")
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Helpers
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def _load_settings(config: str | None) -> Settings:
34
+ """Load settings using a three-tier lookup (first match wins).
35
+
36
+ 1. ``--config <path>`` — explicit CLI flag
37
+ 2. ``.diffrag.toml`` in the current working directory
38
+ 3. :func:`~diffrag.config.settings.user_config_path` — OS user config dir
39
+ 4. Built-in defaults
40
+
41
+ Args:
42
+ config: Optional path to a ``.toml`` configuration file.
43
+
44
+ Returns:
45
+ Resolved :class:`~diffrag.config.Settings` instance.
46
+ """
47
+ if config:
48
+ return Settings.from_toml(Path(config))
49
+ project = Path(".diffrag.toml")
50
+ if project.exists():
51
+ return Settings.from_toml(project)
52
+ user = user_config_path()
53
+ if user.exists():
54
+ return Settings.from_toml(user)
55
+ return Settings.from_defaults()
56
+
57
+
58
+ def _build_reviewer(
59
+ settings: Settings,
60
+ repo_path: Path,
61
+ verbosity: str = "standard",
62
+ ) -> tuple[CodeReviewer, GitRepository | None]:
63
+ """Instantiate the :class:`CodeReviewer` from *settings*.
64
+
65
+ Args:
66
+ settings: Resolved settings object.
67
+ repo_path: Canonical path of the repository being reviewed.
68
+ Used to derive the per-repository database location.
69
+ verbosity: Detail level for generated reviews.
70
+
71
+ Returns:
72
+ A tuple of ``(CodeReviewer, None)``; repository is opened separately.
73
+ """
74
+ ai_client = OpenAICompatClient(
75
+ base_url=settings.ai.base_url,
76
+ model=settings.ai.model,
77
+ api_key=settings.ai.api_key,
78
+ timeout=settings.ai.timeout,
79
+ )
80
+ embed_client = OpenAICompatClient(
81
+ base_url=settings.embedding.base_url,
82
+ model=settings.embedding.model,
83
+ api_key=settings.embedding.api_key,
84
+ timeout=settings.embedding.timeout,
85
+ )
86
+ indexer = RepoIndexer(
87
+ db_path=default_db_path(repo_path),
88
+ embedding_client=embed_client,
89
+ chunk_size=settings.indexing.chunk_size,
90
+ chunk_overlap=settings.indexing.chunk_overlap,
91
+ excluded_patterns=settings.indexing.excluded_patterns,
92
+ max_file_size=settings.indexing.max_file_size,
93
+ )
94
+ reviewer = CodeReviewer(
95
+ ai_client=ai_client,
96
+ indexer=indexer,
97
+ similarity_top_k=settings.review.similarity_top_k,
98
+ max_prompt_tokens=settings.review.max_prompt_tokens,
99
+ verbosity=verbosity,
100
+ )
101
+ return reviewer, None
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # CLI group
106
+ # ---------------------------------------------------------------------------
107
+
108
+
109
+ @click.group()
110
+ @click.option("--verbose", "-v", is_flag=True, help="Enable debug logging.")
111
+ def cli(verbose: bool) -> None:
112
+ """AI-powered git diff analysis and code review with RAG context.
113
+
114
+ Run ``diffrag COMMAND --help`` for details on each command.
115
+ """
116
+ level = logging.DEBUG if verbose else logging.WARNING
117
+ logging.basicConfig(level=level, format="%(levelname)s %(name)s: %(message)s")
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # review command
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ @cli.command()
126
+ @click.option(
127
+ "--repo",
128
+ "-r",
129
+ "repo_path",
130
+ default=".",
131
+ show_default=True,
132
+ type=click.Path(exists=True, file_okay=False),
133
+ help="Path to the git repository.",
134
+ )
135
+ @click.option("--base", "-b", default=None, help="Base branch/ref (default from config or 'main').")
136
+ @click.option("--head", "-H", default="HEAD", show_default=True, help="Head branch/ref to review.")
137
+ @click.option(
138
+ "--config",
139
+ "-c",
140
+ default=None,
141
+ type=click.Path(exists=True),
142
+ help="Path to a .toml config file.",
143
+ )
144
+ @click.option(
145
+ "--verbosity",
146
+ "-V",
147
+ default=None,
148
+ type=click.Choice(["brief", "standard", "detailed"]),
149
+ help="Review detail level (overrides config default).",
150
+ )
151
+ def review(
152
+ repo_path: str,
153
+ base: str | None,
154
+ head: str,
155
+ config: str | None,
156
+ verbosity: str | None,
157
+ ) -> None:
158
+ """Generate an AI code review for changes between two git refs.
159
+
160
+ The repository index is built automatically on the first run and reused on
161
+ subsequent runs as long as HEAD has not changed. A fresh commit triggers a
162
+ rebuild; switching branches also triggers a rebuild.
163
+
164
+ Examples:
165
+
166
+ \b
167
+ diffrag review --base main
168
+ diffrag review --base main --head feature/my-feature
169
+ diffrag review --repo /path/to/repo --base v1.0.0 --head HEAD
170
+ """
171
+ settings = _load_settings(config)
172
+ effective_base = base or settings.review.base_branch
173
+ effective_verbosity = verbosity or settings.review.verbosity
174
+ resolved_repo = Path(repo_path).resolve()
175
+ reviewer, _ = _build_reviewer(settings, repo_path=resolved_repo, verbosity=effective_verbosity)
176
+ repo = GitRepository(Path(repo_path))
177
+
178
+ async def _run() -> None:
179
+ with console.status("Preparing...", spinner="dots") as status:
180
+
181
+ def on_progress(msg: str) -> None:
182
+ status.update(f"[bold cyan]{msg}[/bold cyan]")
183
+ if not msg.endswith("..."):
184
+ console.print(f"[green]✓[/green] {msg}")
185
+
186
+ result = await reviewer.review(
187
+ repo,
188
+ effective_base,
189
+ head,
190
+ on_progress=on_progress,
191
+ )
192
+
193
+ if result.summary == "No changes detected between the two refs.":
194
+ console.print(Panel("[yellow]No changes detected.[/yellow]", title="Review"))
195
+ return
196
+
197
+ n_files = len(result.file_reviews)
198
+ console.print(
199
+ Panel(
200
+ f"[bold]{effective_base}[/bold] → [bold cyan]{head}[/bold cyan]"
201
+ f" [dim]{n_files} file review(s)[/dim]",
202
+ title="[bold blue]AI Code Review[/bold blue]",
203
+ )
204
+ )
205
+
206
+ if len(result.file_reviews) > 1:
207
+ for fr in result.file_reviews:
208
+ console.rule(f"[cyan]{fr.identifier}[/cyan]")
209
+ console.print(Markdown(fr.content))
210
+
211
+ console.rule("[bold]Consolidated Review[/bold]")
212
+
213
+ console.print(Markdown(result.summary))
214
+ console.print(
215
+ f"\n[dim]Tokens — prompt: {result.total_prompt_tokens} "
216
+ f"completion: {result.total_completion_tokens}[/dim]"
217
+ )
218
+
219
+ try:
220
+ asyncio.run(_run())
221
+ except AICodeReviewerError as exc:
222
+ err_console.print(f"Error: {exc}")
223
+ sys.exit(1)
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # ask command
228
+ # ---------------------------------------------------------------------------
229
+
230
+
231
+ @cli.command()
232
+ @click.option(
233
+ "--repo",
234
+ "-r",
235
+ "repo_path",
236
+ default=".",
237
+ show_default=True,
238
+ type=click.Path(exists=True, file_okay=False),
239
+ help="Path to the git repository.",
240
+ )
241
+ @click.option("--base", "-b", default=None, help="Base branch/ref.")
242
+ @click.option("--head", "-H", default="HEAD", show_default=True, help="Head branch/ref.")
243
+ @click.option(
244
+ "--config",
245
+ "-c",
246
+ default=None,
247
+ type=click.Path(exists=True),
248
+ help="Path to a .toml config file.",
249
+ )
250
+ @click.option(
251
+ "--verbosity",
252
+ "-V",
253
+ default=None,
254
+ type=click.Choice(["brief", "standard", "detailed"]),
255
+ help="Answer detail level (overrides config default).",
256
+ )
257
+ def ask(
258
+ repo_path: str,
259
+ base: str | None,
260
+ head: str,
261
+ config: str | None,
262
+ verbosity: str | None,
263
+ ) -> None:
264
+ """Interactively ask questions about the diff and codebase.
265
+
266
+ Launches a REPL where each question is answered using RAG context
267
+ from the indexed repository plus the current diff. Type ``exit`` or
268
+ press Ctrl-D to quit.
269
+
270
+ Example:
271
+
272
+ \b
273
+ diffrag ask --base main
274
+ """
275
+ settings = _load_settings(config)
276
+ effective_base = base or settings.review.base_branch
277
+ effective_verbosity = verbosity or settings.review.verbosity
278
+ resolved_repo = Path(repo_path).resolve()
279
+ reviewer, _ = _build_reviewer(settings, repo_path=resolved_repo, verbosity=effective_verbosity)
280
+ repo = GitRepository(Path(repo_path))
281
+ diff_result = repo.get_diff(effective_base, head)
282
+
283
+ console.print(
284
+ Panel(
285
+ f"Asking about [bold]{effective_base}[/bold] → [bold cyan]{head}[/bold cyan]\n"
286
+ "[dim]Type your question and press Enter. Type 'exit' or Ctrl-D to quit.[/dim]",
287
+ title="[bold green]Interactive Q&A[/bold green]",
288
+ )
289
+ )
290
+
291
+ history: list[tuple[str, str]] = []
292
+ while True:
293
+ try:
294
+ question = console.input("\n[bold cyan]>[/bold cyan] ").strip()
295
+ except (EOFError, KeyboardInterrupt):
296
+ console.print("\n[dim]Bye.[/dim]")
297
+ break
298
+
299
+ if question.lower() in ("exit", "quit", "q"):
300
+ break
301
+ if not question:
302
+ continue
303
+
304
+ async def _answer(q: str = question, hist: list[tuple[str, str]] = history) -> str:
305
+ with console.status("[bold green]Thinking…[/]", spinner="dots"):
306
+ return await reviewer.ask(q, diff_result, history=hist)
307
+
308
+ try:
309
+ answer = asyncio.run(_answer())
310
+ console.print(Markdown(answer))
311
+ history.append((question, answer))
312
+ except AICodeReviewerError as exc:
313
+ err_console.print(f"Error: {exc}")
314
+
315
+
316
+ # ---------------------------------------------------------------------------
317
+ # create-config command
318
+ # ---------------------------------------------------------------------------
319
+
320
+ _EXAMPLE_CONFIG = importlib.resources.files("diffrag").joinpath("data/config.example.toml")
321
+
322
+
323
+ @cli.command("create-config")
324
+ @click.option(
325
+ "--out",
326
+ "-o",
327
+ default=None,
328
+ type=click.Path(dir_okay=False),
329
+ help="Destination path (default: user-level config returned by config-path).",
330
+ )
331
+ @click.option("--force", "-f", is_flag=True, help="Overwrite if the file already exists.")
332
+ def create_config(out: str | None, force: bool) -> None:
333
+ """Create a config file pre-populated with all available settings.
334
+
335
+ By default the file is written to the user-level config directory so it
336
+ applies to every repository on this machine. Pass ``--out`` to write a
337
+ project-level ``.diffrag.toml`` instead.
338
+
339
+ \b
340
+ diffrag create-config # global user config
341
+ diffrag create-config --out .diffrag.toml # project override
342
+ diffrag create-config --force # overwrite existing
343
+ """
344
+ dest = Path(out) if out else user_config_path()
345
+
346
+ if dest.exists() and not force:
347
+ err_console.print(
348
+ f"[bold]{dest}[/bold] already exists. Use [bold]--force[/bold] to overwrite."
349
+ )
350
+ sys.exit(1)
351
+
352
+ dest.parent.mkdir(parents=True, exist_ok=True)
353
+ dest.write_text(_EXAMPLE_CONFIG.read_text(encoding="utf-8"), encoding="utf-8")
354
+ console.print(f"[green]✓[/green] Config written to [bold]{dest}[/bold]")
355
+
356
+
357
+ # ---------------------------------------------------------------------------
358
+ # db-path command
359
+ # ---------------------------------------------------------------------------
360
+
361
+
362
+ @cli.command("db-path")
363
+ @click.option(
364
+ "--repo",
365
+ "-r",
366
+ "repo_path",
367
+ default=".",
368
+ show_default=True,
369
+ type=click.Path(exists=True, file_okay=False),
370
+ help="Path to the git repository.",
371
+ )
372
+ def db_path_cmd(repo_path: str) -> None:
373
+ """Print the database path for a repository.
374
+
375
+ The index for each repository is stored in a unique subdirectory of the
376
+ OS user cache directory, derived from the repository's canonical path.
377
+ The directory may not exist yet (it is created on first run).
378
+
379
+ \b
380
+ diffrag db-path
381
+ diffrag db-path --repo /path/to/repo
382
+ """
383
+ console.print(str(default_db_path(Path(repo_path).resolve())))
384
+
385
+
386
+ # ---------------------------------------------------------------------------
387
+ # config-path command
388
+ # ---------------------------------------------------------------------------
389
+
390
+
391
+ @cli.command("config-path")
392
+ def config_path_cmd() -> None:
393
+ """Print the path to the user-level config file."""
394
+ console.print(str(user_config_path()))
395
+
396
+
397
+ # ---------------------------------------------------------------------------
398
+ # show-config command
399
+ # ---------------------------------------------------------------------------
400
+
401
+
402
+ @cli.command("show-config")
403
+ @click.option(
404
+ "--config",
405
+ "-c",
406
+ default=None,
407
+ type=click.Path(exists=True),
408
+ help="Path to a .toml config file.",
409
+ )
410
+ def show_config(config: str | None) -> None:
411
+ """Display the effective configuration as JSON.
412
+
413
+ Useful for verifying which settings are active, including defaults.
414
+ """
415
+ settings = _load_settings(config)
416
+
417
+ def _serialise(obj: object) -> object:
418
+ if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
419
+ return {f.name: _serialise(getattr(obj, f.name)) for f in dataclasses.fields(obj)}
420
+ if isinstance(obj, Path):
421
+ return str(obj)
422
+ if isinstance(obj, list):
423
+ return [_serialise(v) for v in obj]
424
+ return obj
425
+
426
+ table = Table(title="Effective Configuration", show_header=True)
427
+ table.add_column("Section", style="cyan")
428
+ table.add_column("Key", style="white")
429
+ table.add_column("Value", style="green")
430
+
431
+ raw = _serialise(settings)
432
+ assert isinstance(raw, dict)
433
+ for section, values in raw.items():
434
+ if isinstance(values, dict):
435
+ for key, value in values.items():
436
+ table.add_row(str(section), str(key), json.dumps(value))
437
+ else:
438
+ table.add_row("", str(section), json.dumps(values))
439
+
440
+ console.print(table)
@@ -0,0 +1,21 @@
1
+ """Configuration loading and data models."""
2
+
3
+ from .settings import (
4
+ AISettings,
5
+ EmbeddingSettings,
6
+ IndexingSettings,
7
+ ReviewSettings,
8
+ Settings,
9
+ default_db_path,
10
+ user_config_path,
11
+ )
12
+
13
+ __all__ = [
14
+ "AISettings",
15
+ "EmbeddingSettings",
16
+ "IndexingSettings",
17
+ "ReviewSettings",
18
+ "Settings",
19
+ "default_db_path",
20
+ "user_config_path",
21
+ ]
@@ -0,0 +1,190 @@
1
+ """Settings loaded from a TOML configuration file."""
2
+
3
+ import hashlib
4
+ import logging
5
+ import tomllib
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ import platformdirs
10
+
11
+ from ..exceptions import ConfigurationError
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def user_config_path() -> Path:
17
+ """Return the OS-appropriate user-level config file path.
18
+
19
+ - Linux / macOS: ``~/.config/diffrag/config.toml``
20
+ - Windows: ``%APPDATA%\\diffrag\\config.toml``
21
+
22
+ The file may not exist yet; call this to find where to create it.
23
+ """
24
+ return Path(platformdirs.user_config_dir("diffrag")) / "config.toml"
25
+
26
+
27
+ def default_db_path(repo_path: Path) -> Path:
28
+ """Return the cache-directory DB path for *repo_path*.
29
+
30
+ Each repository maps to a unique subdirectory inside the OS user cache dir,
31
+ named after the first 12 hex characters of the SHA-256 hash of the repo's
32
+ canonical absolute path.
33
+
34
+ - Linux / macOS: ``~/.cache/diffrag/<hash>/``
35
+ - Windows: ``%LOCALAPPDATA%\\diffrag\\Cache\\<hash>\\``
36
+ """
37
+ digest = hashlib.sha256(str(repo_path.resolve()).encode()).hexdigest()[:12]
38
+ return Path(platformdirs.user_cache_dir("diffrag")) / digest
39
+
40
+
41
+ _DEFAULT_EXCLUDED_PATTERNS = [
42
+ ".git",
43
+ ".venv",
44
+ "__pycache__",
45
+ "node_modules",
46
+ ".idea",
47
+ ".mypy_cache",
48
+ ".ruff_cache",
49
+ "dist",
50
+ "build",
51
+ ]
52
+
53
+
54
+ @dataclass
55
+ class AISettings:
56
+ """Settings for the language model used to generate reviews.
57
+
58
+ Attributes:
59
+ base_url: Base URL of the OpenAI-compatible or Ollama endpoint.
60
+ model: Model identifier to use for completion requests.
61
+ api_key: Bearer token sent in the Authorization header (empty = none).
62
+ timeout: HTTP request timeout in seconds.
63
+ """
64
+
65
+ base_url: str = "http://localhost:11434"
66
+ model: str = "llama3.2"
67
+ api_key: str = ""
68
+ timeout: float = 120.0
69
+
70
+
71
+ @dataclass
72
+ class EmbeddingSettings:
73
+ """Settings for the embedding model used to build the vector index.
74
+
75
+ Attributes:
76
+ base_url: Base URL of the OpenAI-compatible or Ollama endpoint.
77
+ model: Model identifier to use for embedding requests.
78
+ api_key: Bearer token sent in the Authorization header (empty = none).
79
+ timeout: HTTP request timeout in seconds.
80
+ """
81
+
82
+ base_url: str = "http://localhost:11434"
83
+ model: str = "nomic-embed-text"
84
+ api_key: str = ""
85
+ timeout: float = 60.0
86
+
87
+
88
+ @dataclass
89
+ class IndexingSettings:
90
+ """Settings that control how the repository is indexed.
91
+
92
+ The database location is always determined automatically by
93
+ :func:`default_db_path` and cannot be configured.
94
+
95
+ Attributes:
96
+ chunk_size: Maximum number of characters per text chunk.
97
+ chunk_overlap: Number of characters to overlap between consecutive chunks.
98
+ excluded_patterns: Additional directory or file name segments to skip,
99
+ applied on top of any patterns found in the repository's ``.gitignore``.
100
+ ``.git`` is always excluded regardless of this setting.
101
+ max_file_size: Files larger than this (bytes) are skipped.
102
+ """
103
+
104
+ chunk_size: int = 1024
105
+ chunk_overlap: int = 128
106
+ excluded_patterns: list[str] = field(default_factory=lambda: list(_DEFAULT_EXCLUDED_PATTERNS))
107
+ max_file_size: int = 1_000_000
108
+
109
+
110
+ @dataclass
111
+ class ReviewSettings:
112
+ """Settings that control the review behaviour.
113
+
114
+ Attributes:
115
+ base_branch: Default branch to diff against when none is specified.
116
+ similarity_top_k: Number of RAG context chunks retrieved per diff chunk.
117
+ max_prompt_tokens: Approximate token budget per diff chunk before splitting.
118
+ verbosity: Detail level of generated reviews. One of ``"brief"``,
119
+ ``"standard"`` (default), or ``"detailed"``.
120
+ """
121
+
122
+ base_branch: str = "main"
123
+ similarity_top_k: int = 5
124
+ max_prompt_tokens: int = 6000
125
+ verbosity: str = "standard"
126
+
127
+ def __post_init__(self) -> None:
128
+ if self.verbosity not in ("brief", "standard", "detailed"):
129
+ raise ConfigurationError(
130
+ f"Invalid verbosity {self.verbosity!r}. Choose 'brief', 'standard', or 'detailed'."
131
+ )
132
+
133
+
134
+ @dataclass
135
+ class Settings:
136
+ """Root settings object aggregating all configuration sections.
137
+
138
+ Attributes:
139
+ ai: Language model settings.
140
+ embedding: Embedding model settings.
141
+ indexing: Vector index settings.
142
+ review: Review behaviour settings.
143
+ """
144
+
145
+ ai: AISettings = field(default_factory=AISettings)
146
+ embedding: EmbeddingSettings = field(default_factory=EmbeddingSettings)
147
+ indexing: IndexingSettings = field(default_factory=IndexingSettings)
148
+ review: ReviewSettings = field(default_factory=ReviewSettings)
149
+
150
+ @classmethod
151
+ def from_toml(cls, path: Path) -> "Settings":
152
+ """Load settings from a TOML file.
153
+
154
+ Args:
155
+ path: Path to the ``.toml`` configuration file.
156
+
157
+ Returns:
158
+ A fully populated ``Settings`` instance.
159
+
160
+ Raises:
161
+ ConfigurationError: If the file cannot be read or contains invalid keys.
162
+ """
163
+ try:
164
+ with path.open("rb") as fh:
165
+ data = tomllib.load(fh)
166
+ except OSError as exc:
167
+ raise ConfigurationError(f"Cannot read config file {path}: {exc}") from exc
168
+ except tomllib.TOMLDecodeError as exc:
169
+ raise ConfigurationError(f"Invalid TOML in {path}: {exc}") from exc
170
+
171
+ log.debug("Loaded configuration from %s", path)
172
+
173
+ try:
174
+ return cls(
175
+ ai=AISettings(**data.get("ai", {})),
176
+ embedding=EmbeddingSettings(**data.get("embedding", {})),
177
+ indexing=IndexingSettings(**data.get("indexing", {})),
178
+ review=ReviewSettings(**data.get("review", {})),
179
+ )
180
+ except TypeError as exc:
181
+ raise ConfigurationError(f"Unexpected key in config {path}: {exc}") from exc
182
+
183
+ @classmethod
184
+ def from_defaults(cls) -> "Settings":
185
+ """Return a ``Settings`` instance with all default values.
186
+
187
+ Returns:
188
+ A ``Settings`` instance populated with default values.
189
+ """
190
+ return cls()
@@ -0,0 +1,53 @@
1
+ # diffrag configuration
2
+ #
3
+ # Settings are loaded from the first file found:
4
+ # 1. --config <path> (explicit CLI flag)
5
+ # 2. .diffrag.toml (current working directory)
6
+ # 3. user-level config (run "diffrag config-path" to find this)
7
+ # 4. Built-in defaults (no file needed)
8
+ #
9
+ # All keys are optional — remove any section to use built-in defaults.
10
+
11
+ [ai]
12
+ # Base URL of the OpenAI-compatible endpoint (OpenWebUI, Ollama /v1, OpenAI, etc.)
13
+ base_url = "http://localhost:11434"
14
+ # Model identifier to use for review generation
15
+ model = "llama3.2"
16
+ # API key / bearer token (leave empty for unauthenticated local servers)
17
+ api_key = ""
18
+ # Per-read timeout in seconds (requests use streaming; this is the max time
19
+ # between consecutive chunks, not the total generation time)
20
+ timeout = 120.0
21
+
22
+ [embedding]
23
+ # Embedding endpoint — can differ from the completion endpoint
24
+ base_url = "http://localhost:11434"
25
+ model = "nomic-embed-text"
26
+ api_key = ""
27
+ timeout = 60.0
28
+
29
+ [indexing]
30
+ # The index is always stored in the OS user cache directory, keyed by repo.
31
+ # Run "diffrag db-path" to see the exact path for the current repository.
32
+ # Target chunk size in characters
33
+ chunk_size = 1024
34
+ # Overlap between consecutive chunks in characters
35
+ chunk_overlap = 128
36
+ # Patterns in the repository's .gitignore are excluded automatically.
37
+ # The list below provides additional excludes on top of .gitignore.
38
+ excluded_patterns = [
39
+ ".git", ".venv", "__pycache__", "node_modules",
40
+ ".idea", ".mypy_cache", ".ruff_cache", "dist", "build",
41
+ ]
42
+ # Files larger than this (bytes) are not indexed
43
+ max_file_size = 1000000
44
+
45
+ [review]
46
+ # Default branch to diff against when --base is not specified
47
+ base_branch = "main"
48
+ # Number of context chunks retrieved from the index per diff chunk
49
+ similarity_top_k = 5
50
+ # Approximate token budget per diff chunk before hunk-splitting kicks in
51
+ max_prompt_tokens = 6000
52
+ # Detail level of generated reviews: "brief" | "standard" | "detailed"
53
+ verbosity = "standard"