bugbee 0.1.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.
- bugbee/__init__.py +13 -0
- bugbee/cache/__init__.py +76 -0
- bugbee/cli/__init__.py +0 -0
- bugbee/cli/main.py +258 -0
- bugbee/config/settings.py +116 -0
- bugbee/lazy.py +42 -0
- bugbee/legacy.py +31 -0
- bugbee/llm/__init__.py +59 -0
- bugbee/metrics/__init__.py +58 -0
- bugbee/parser/__init__.py +0 -0
- bugbee/parser/error_parser.py +106 -0
- bugbee/patcher/__init__.py +61 -0
- bugbee/retrieval/__init__.py +91 -0
- bugbee/ui.py +239 -0
- bugbee/utils/logging.py +90 -0
- bugbee/validator/__init__.py +30 -0
- bugbee-0.1.0.dist-info/METADATA +292 -0
- bugbee-0.1.0.dist-info/RECORD +22 -0
- bugbee-0.1.0.dist-info/WHEEL +5 -0
- bugbee-0.1.0.dist-info/entry_points.txt +2 -0
- bugbee-0.1.0.dist-info/licenses/LICENSE +209 -0
- bugbee-0.1.0.dist-info/top_level.txt +1 -0
bugbee/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Top level package for BugBee.
|
|
2
|
+
|
|
3
|
+
Provides version information and exposes the public API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version(__name__)
|
|
10
|
+
except PackageNotFoundError: # pragma: no cover – during development the package may not be installed yet
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["__version__"]
|
bugbee/cache/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Cache handling for BugBee.
|
|
2
|
+
|
|
3
|
+
Provides a thin wrapper around a JSON file (default ``bugbee.json``) used to
|
|
4
|
+
store LLM responses keyed by a deterministic error hash. The API mirrors the
|
|
5
|
+
original script's ``check_cache`` and ``save_to_cache`` functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from bugbee.config.settings import settings
|
|
15
|
+
|
|
16
|
+
__all__ = ["Cache", "cache"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Cache:
|
|
20
|
+
"""Simple JSON‑file cache.
|
|
21
|
+
|
|
22
|
+
The cache file is created on first use and persisted between runs. Loading
|
|
23
|
+
and writing are performed lazily to avoid disk I/O during CLI start‑up.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, path: Optional[Path] = None) -> None:
|
|
27
|
+
self.path = path or settings.cache_file
|
|
28
|
+
self._data: Dict[str, Any] = {}
|
|
29
|
+
self._loaded = False
|
|
30
|
+
|
|
31
|
+
def _load(self) -> None:
|
|
32
|
+
if self._loaded:
|
|
33
|
+
return
|
|
34
|
+
if self.path.is_file():
|
|
35
|
+
try:
|
|
36
|
+
self._data = json.load(self.path.open())
|
|
37
|
+
except json.JSONDecodeError:
|
|
38
|
+
self._data = {}
|
|
39
|
+
else:
|
|
40
|
+
self._data = {}
|
|
41
|
+
self._loaded = True
|
|
42
|
+
|
|
43
|
+
def _save(self) -> None:
|
|
44
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
self.path.write_text(json.dumps(self._data, indent=2))
|
|
46
|
+
|
|
47
|
+
def get(self, key: str) -> Optional[Any]:
|
|
48
|
+
self._load()
|
|
49
|
+
return self._data.get(key)
|
|
50
|
+
|
|
51
|
+
def set(self, key: str, value: Any) -> None:
|
|
52
|
+
self._load()
|
|
53
|
+
self._data[key] = value
|
|
54
|
+
self._save()
|
|
55
|
+
|
|
56
|
+
def clear(self) -> None:
|
|
57
|
+
self._data.clear()
|
|
58
|
+
self._loaded = True
|
|
59
|
+
if self.path.is_file():
|
|
60
|
+
self.path.unlink()
|
|
61
|
+
|
|
62
|
+
# Export a lazy accessor for the cache. The cache object is instantiated
|
|
63
|
+
# only when first needed, avoiding file I/O during CLI start‑up.
|
|
64
|
+
|
|
65
|
+
def get_cache() -> Cache:
|
|
66
|
+
"""Return a shared ``Cache`` instance, creating it on first call.
|
|
67
|
+
|
|
68
|
+
This mirrors the previous ``cache`` singleton but defers the actual object
|
|
69
|
+
construction (and the associated disk read) until the cache is accessed.
|
|
70
|
+
"""
|
|
71
|
+
global _cache_instance
|
|
72
|
+
try:
|
|
73
|
+
return _cache_instance
|
|
74
|
+
except NameError:
|
|
75
|
+
_cache_instance = Cache()
|
|
76
|
+
return _cache_instance
|
bugbee/cli/__init__.py
ADDED
|
File without changes
|
bugbee/cli/main.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""CLI entry point for the BugBee package.
|
|
2
|
+
|
|
3
|
+
The original monolithic implementation lived in ``cli-wrapper/src/cli_wrapper/main.py``.
|
|
4
|
+
This version keeps the ``run`` command as a clean pipeline built on top of:
|
|
5
|
+
|
|
6
|
+
* ``bugbee.parser.error_parser`` – stack‑trace extraction, sanitisation, hashing
|
|
7
|
+
* ``bugbee.cache`` – JSON cache wrapper
|
|
8
|
+
* ``bugbee.retrieval`` – ChromaDB document retrieval
|
|
9
|
+
* ``bugbee.llm`` – LLM provider abstraction
|
|
10
|
+
* ``bugbee.patcher`` – safe patch application with backup
|
|
11
|
+
* ``bugbee.validator`` – post‑patch validation runner
|
|
12
|
+
* ``bugbee.metrics`` – metrics collection and JSONL persistence
|
|
13
|
+
* ``bugbee.ui`` – ALL logging and terminal presentation (spinners, colored
|
|
14
|
+
status lines, panels, tables, confirmation prompts) lives here. This file
|
|
15
|
+
contains no direct ``log.*`` or ``typer.echo`` calls — every user-facing
|
|
16
|
+
message is routed through the ``ui`` singleton so the pipeline below reads
|
|
17
|
+
as pure business logic.
|
|
18
|
+
|
|
19
|
+
The command line mimics the historic behaviour while being fully testable,
|
|
20
|
+
type‑safe, and pleasant to actually use.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import List, Tuple
|
|
29
|
+
|
|
30
|
+
import typer
|
|
31
|
+
|
|
32
|
+
from bugbee import __version__
|
|
33
|
+
from bugbee.config.settings import settings
|
|
34
|
+
from bugbee.parser.error_parser import (
|
|
35
|
+
extract_location,
|
|
36
|
+
sanitize_error,
|
|
37
|
+
error_hash,
|
|
38
|
+
)
|
|
39
|
+
from bugbee.cache import get_cache
|
|
40
|
+
from bugbee.lazy import lazy_import
|
|
41
|
+
from bugbee.ui import ui
|
|
42
|
+
from bugbee.utils.logging import enable_console_logging
|
|
43
|
+
# Heavy imports are lazy‑loaded inside the command handlers.
|
|
44
|
+
# from bugbee.retrieval import get_related_docs
|
|
45
|
+
# from bugbee.llm import default_provider
|
|
46
|
+
# from bugbee.metrics import MetricsCollector
|
|
47
|
+
# from bugbee.patcher import apply_patch
|
|
48
|
+
# from bugbee.validator import run_validation
|
|
49
|
+
|
|
50
|
+
app = typer.Typer(
|
|
51
|
+
help="BugBee – AI‑powered CLI debugger and auto‑repair tool",
|
|
52
|
+
rich_markup_mode="rich",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.callback()
|
|
57
|
+
def main(
|
|
58
|
+
verbose: bool = typer.Option(
|
|
59
|
+
False,
|
|
60
|
+
"--verbose",
|
|
61
|
+
"-v",
|
|
62
|
+
help="Also print raw, timestamped log lines to the console (in addition to the normal UI).",
|
|
63
|
+
),
|
|
64
|
+
) -> None:
|
|
65
|
+
"""BugBee – AI‑powered CLI debugger and auto‑repair tool."""
|
|
66
|
+
if verbose:
|
|
67
|
+
enable_console_logging()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _run_subprocess(command: List[str]) -> Tuple[int, str, str]:
|
|
71
|
+
"""Execute *command* and return ``(returncode, stdout, stderr)``.
|
|
72
|
+
|
|
73
|
+
The function deliberately forwards stdout directly to the console (as the
|
|
74
|
+
original tool did) and captures stderr for analysis.
|
|
75
|
+
"""
|
|
76
|
+
import subprocess
|
|
77
|
+
|
|
78
|
+
ui.running_command(command)
|
|
79
|
+
proc = subprocess.Popen(
|
|
80
|
+
command,
|
|
81
|
+
stdout=subprocess.PIPE,
|
|
82
|
+
stderr=subprocess.PIPE,
|
|
83
|
+
text=True,
|
|
84
|
+
)
|
|
85
|
+
out, err = proc.communicate()
|
|
86
|
+
# Echo stdout to the console to preserve original behaviour.
|
|
87
|
+
if out:
|
|
88
|
+
sys.stdout.write(out)
|
|
89
|
+
return proc.returncode, out, err
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_llm_output(output: str) -> Tuple[str, str, str]:
|
|
93
|
+
"""Extract ``error_line`` and ``fix`` from the LLM ``output``.
|
|
94
|
+
|
|
95
|
+
The original script expected XML‑like tags <ERROR_LINE> and <FIX>. We keep
|
|
96
|
+
that contract for backward compatibility.
|
|
97
|
+
"""
|
|
98
|
+
error_line_match = re.search(r"<ERROR_LINE>(.*?)</ERROR_LINE>", output, re.DOTALL)
|
|
99
|
+
fix_match = re.search(r"<FIX>(.*?)</FIX>", output, re.DOTALL)
|
|
100
|
+
error_line = error_line_match.group(1).strip() if error_line_match else ""
|
|
101
|
+
fix = fix_match.group(1).strip() if fix_match else ""
|
|
102
|
+
# The full content without the tags is also useful for reporting.
|
|
103
|
+
content = output.strip()
|
|
104
|
+
return content, error_line, fix
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command()
|
|
108
|
+
def run(
|
|
109
|
+
command: List[str] = typer.Argument(..., help="Command to execute and debug"),
|
|
110
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Auto‑accept fix without prompting"),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Execute *command*, analyse failures and optionally apply an auto‑fix."""
|
|
113
|
+
# Lazy imports of heavy modules – they are only loaded when this command runs.
|
|
114
|
+
MetricsCollector = lazy_import('bugbee.metrics').MetricsCollector
|
|
115
|
+
get_related_docs = lazy_import('bugbee.retrieval').get_related_docs
|
|
116
|
+
default_provider = lazy_import('bugbee.llm').default_provider
|
|
117
|
+
apply_patch = lazy_import('bugbee.patcher').apply_patch
|
|
118
|
+
run_validation = lazy_import('bugbee.validator').run_validation
|
|
119
|
+
|
|
120
|
+
metrics = MetricsCollector()
|
|
121
|
+
metrics.update("command", " ".join(command))
|
|
122
|
+
|
|
123
|
+
returncode, _, stderr = _run_subprocess(command)
|
|
124
|
+
if returncode == 0:
|
|
125
|
+
ui.command_succeeded()
|
|
126
|
+
metrics.write()
|
|
127
|
+
raise typer.Exit(code=0)
|
|
128
|
+
|
|
129
|
+
ui.command_failed()
|
|
130
|
+
|
|
131
|
+
# 1️⃣ Extract location info (file + line) – may be ``None``.
|
|
132
|
+
file_path, line_number = extract_location(stderr, command)
|
|
133
|
+
if file_path and line_number:
|
|
134
|
+
ui.location_found(file_path, line_number)
|
|
135
|
+
else:
|
|
136
|
+
ui.location_not_found()
|
|
137
|
+
|
|
138
|
+
# 2️⃣ Sanitize error and compute hash for caching.
|
|
139
|
+
cleaned = sanitize_error(stderr)
|
|
140
|
+
err_hash = error_hash(cleaned)
|
|
141
|
+
cache_obj = get_cache()
|
|
142
|
+
cached = cache_obj.get(err_hash)
|
|
143
|
+
if cached:
|
|
144
|
+
ui.cache_hit()
|
|
145
|
+
llm_content, error_line, fix = cached
|
|
146
|
+
else:
|
|
147
|
+
ui.cache_miss()
|
|
148
|
+
# Retrieve relevant docs.
|
|
149
|
+
with ui.spinner("Retrieving relevant documentation…"):
|
|
150
|
+
docs, top_score, avg_score = get_related_docs(stderr)
|
|
151
|
+
metrics.update("retrieval.top_score", top_score)
|
|
152
|
+
metrics.update("retrieval.avg_score", avg_score)
|
|
153
|
+
ui.retrieval_stats(top_score, avg_score)
|
|
154
|
+
|
|
155
|
+
# Very simple prompt composition.
|
|
156
|
+
prompt = (
|
|
157
|
+
f"Error message:\n{stderr}\n\n"
|
|
158
|
+
f"Relevant documentation:\n{docs}\n\n"
|
|
159
|
+
"Provide an XML-like response containing <ERROR_LINE> and <FIX>."
|
|
160
|
+
)
|
|
161
|
+
with ui.spinner("Consulting the model for a fix…"):
|
|
162
|
+
llm_resp = default_provider().generate(prompt)
|
|
163
|
+
|
|
164
|
+
llm_content, error_line, fix = _parse_llm_output(llm_resp)
|
|
165
|
+
cache_obj.set(err_hash, (llm_content, error_line, fix))
|
|
166
|
+
|
|
167
|
+
# Show the LLM analysis to the user.
|
|
168
|
+
ui.show_analysis(llm_content)
|
|
169
|
+
|
|
170
|
+
# Auto‑fix handling.
|
|
171
|
+
performed = False
|
|
172
|
+
accept = ui.confirm_fix(yes)
|
|
173
|
+
if accept and file_path:
|
|
174
|
+
ui.applying_patch(file_path, line_number)
|
|
175
|
+
try:
|
|
176
|
+
success = apply_patch(file_path, line_number, error_line, fix + "\n", create_backup=True)
|
|
177
|
+
if success:
|
|
178
|
+
performed = True
|
|
179
|
+
metrics.update("autofix.patch_applied", True)
|
|
180
|
+
metrics.update("autofix.files_modified", 1)
|
|
181
|
+
ui.patch_applied()
|
|
182
|
+
# Run validation to ensure fix works.
|
|
183
|
+
with ui.spinner("Validating the fix…"):
|
|
184
|
+
validation_ok = run_validation(command, cwd=Path.cwd())
|
|
185
|
+
if validation_ok:
|
|
186
|
+
metrics.update("validation.passed", True)
|
|
187
|
+
ui.validation_passed()
|
|
188
|
+
else:
|
|
189
|
+
ui.validation_failed()
|
|
190
|
+
else:
|
|
191
|
+
ui.patch_not_applied()
|
|
192
|
+
except Exception as exc:
|
|
193
|
+
ui.patch_error(exc)
|
|
194
|
+
else:
|
|
195
|
+
ui.auto_fix_declined()
|
|
196
|
+
|
|
197
|
+
# Record final metrics.
|
|
198
|
+
metrics.update("diagnosis.error_line_found", bool(error_line))
|
|
199
|
+
metrics.update("diagnosis.fix_generated", bool(fix))
|
|
200
|
+
metrics.write()
|
|
201
|
+
|
|
202
|
+
ui.summary(fix_applied=performed, returncode=returncode)
|
|
203
|
+
|
|
204
|
+
# Exit with the original return code if no successful fix was applied.
|
|
205
|
+
if performed:
|
|
206
|
+
raise typer.Exit(code=0)
|
|
207
|
+
else:
|
|
208
|
+
raise typer.Exit(code=returncode)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command()
|
|
212
|
+
def analyze(
|
|
213
|
+
command: List[str] = typer.Argument(..., help="Command to analyze without applying a fix"),
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Run *command* and display LLM analysis but do not prompt for auto‑fix."""
|
|
216
|
+
# Reuse ``run`` logic but force ``yes=False`` and skip patch step.
|
|
217
|
+
run(command=command, yes=False)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command()
|
|
221
|
+
def version() -> None:
|
|
222
|
+
"""Print the installed BugBee version."""
|
|
223
|
+
ui.version(__version__)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@app.command()
|
|
227
|
+
def cache_clear() -> None:
|
|
228
|
+
"""Remove the local cache file (``bugbee.json``)."""
|
|
229
|
+
get_cache().clear()
|
|
230
|
+
ui.cache_cleared()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.command()
|
|
234
|
+
def config_show() -> None:
|
|
235
|
+
"""Display the effective configuration (environment variables, .env, defaults)."""
|
|
236
|
+
ui.config_table(settings.dict())
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.command()
|
|
240
|
+
def doctor() -> None:
|
|
241
|
+
"""Run a quick health‑check of required dependencies and connectivity."""
|
|
242
|
+
ui.doctor_start()
|
|
243
|
+
missing = []
|
|
244
|
+
try:
|
|
245
|
+
import chromadb # noqa: F401
|
|
246
|
+
except Exception:
|
|
247
|
+
missing.append("chromadb")
|
|
248
|
+
try:
|
|
249
|
+
import langchain # noqa: F401
|
|
250
|
+
except Exception:
|
|
251
|
+
missing.append("langchain")
|
|
252
|
+
ui.doctor_result(missing)
|
|
253
|
+
if missing:
|
|
254
|
+
raise typer.Exit(code=1)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__":
|
|
258
|
+
app()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Configuration handling for BugBee.
|
|
2
|
+
|
|
3
|
+
BugBee loads configuration from:
|
|
4
|
+
|
|
5
|
+
1. Environment variables
|
|
6
|
+
2. .env file
|
|
7
|
+
3. Optional YAML configuration
|
|
8
|
+
|
|
9
|
+
Environment variables always take precedence.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Mapping
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
from pydantic import Field
|
|
19
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
20
|
+
from platformdirs import user_cache_dir, user_data_dir
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Settings(BaseSettings):
|
|
24
|
+
"""Global configuration for BugBee."""
|
|
25
|
+
|
|
26
|
+
# ------------------------------------------------------------------
|
|
27
|
+
# Pydantic Settings Configuration
|
|
28
|
+
# ------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
model_config = SettingsConfigDict(
|
|
31
|
+
env_file=".env",
|
|
32
|
+
env_file_encoding="utf-8",
|
|
33
|
+
case_sensitive=False,
|
|
34
|
+
extra="ignore", # Ignore unknown environment variables
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
# General
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
log_level: str = "INFO"
|
|
42
|
+
|
|
43
|
+
temperature: float = 0.1
|
|
44
|
+
|
|
45
|
+
retrieval_k: int = 3
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
# Paths
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
cache_file: Path = Field(
|
|
52
|
+
default_factory=lambda: Path(user_cache_dir("bugbee")) / "cache.json"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
chromadb_path: Path = Field(
|
|
56
|
+
default_factory=lambda: Path(user_data_dir("bugbee")) / "chromadb"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
# LLM Configuration
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
llm_endpoint: str = (
|
|
64
|
+
"https://api-inference.huggingface.co/models/deepseek-ai/DeepSeek-R1"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
# API Keys
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
openai_api_key: str | None = Field(
|
|
72
|
+
default=None,
|
|
73
|
+
alias="OPENAI_API_KEY",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
anthropic_api_key: str | None = Field(
|
|
77
|
+
default=None,
|
|
78
|
+
alias="ANTHROPIC_API_KEY",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
google_api_key: str | None = Field(
|
|
82
|
+
default=None,
|
|
83
|
+
alias="GOOGLE_API_KEY",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
openrouter_api_key: str | None = Field(
|
|
87
|
+
default=None,
|
|
88
|
+
alias="OPENROUTER_API_KEY",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
huggingface_api_key: str | None = Field(
|
|
92
|
+
default=None,
|
|
93
|
+
validation_alias="HF_TOKEN",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# Optional YAML Loader
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_yaml(cls, yaml_path: Path) -> "Settings":
|
|
103
|
+
"""Load configuration from a YAML file."""
|
|
104
|
+
|
|
105
|
+
if not yaml_path.exists():
|
|
106
|
+
return cls()
|
|
107
|
+
|
|
108
|
+
data: Mapping[str, Any] = yaml.safe_load(
|
|
109
|
+
yaml_path.read_text(encoding="utf-8")
|
|
110
|
+
) or {}
|
|
111
|
+
|
|
112
|
+
return cls(**data)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Singleton instance used across the project.
|
|
116
|
+
settings = Settings()
|
bugbee/lazy.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Utility helpers for lazy importing heavy dependencies.
|
|
2
|
+
|
|
3
|
+
This module provides a simple ``lazy_import`` function that defers the actual
|
|
4
|
+
``import`` until the first attribute access. It is used throughout the codebase
|
|
5
|
+
to keep the CLI start‑up path lightweight.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import sys
|
|
12
|
+
import types
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def lazy_import(module_name: str) -> types.ModuleType:
|
|
17
|
+
"""Return a proxy module that imports ``module_name`` on first attribute use.
|
|
18
|
+
|
|
19
|
+
The returned object behaves like the real module but delays the import until
|
|
20
|
+
an attribute or ``__getattr__`` is accessed. Subsequent accesses reuse the
|
|
21
|
+
already-loaded module.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
class _LazyModule(types.ModuleType):
|
|
25
|
+
__dict__: dict[str, Any]
|
|
26
|
+
_module: types.ModuleType | None = None
|
|
27
|
+
|
|
28
|
+
def _load(self) -> types.ModuleType:
|
|
29
|
+
if self._module is None:
|
|
30
|
+
self._module = importlib.import_module(module_name)
|
|
31
|
+
# Insert the loaded module into ``sys.modules`` so other imports
|
|
32
|
+
# resolve to the same object.
|
|
33
|
+
sys.modules[module_name] = self._module
|
|
34
|
+
return self._module
|
|
35
|
+
|
|
36
|
+
def __getattr__(self, name: str) -> Any: # pragma: no cover – exercised at runtime
|
|
37
|
+
return getattr(self._load(), name)
|
|
38
|
+
|
|
39
|
+
def __dir__(self) -> list[str]: # pragma: no cover
|
|
40
|
+
return dir(self._load())
|
|
41
|
+
|
|
42
|
+
return _LazyModule(module_name)
|
bugbee/legacy.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Legacy shim for backward compatibility.
|
|
2
|
+
|
|
3
|
+
The original project exposed a ``main`` function in ``cli-wrapper/src/cli_wrapper/main.py``.
|
|
4
|
+
We import that function and expose it here so existing ``import bugbee.legacy``
|
|
5
|
+
or ``python -m bugbee.legacy`` continues to work.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Ensure the original source directory is on ``sys.path``.
|
|
12
|
+
_PROJECT_ROOT = Path(__file__).resolve().parents[2] # points to repository root
|
|
13
|
+
_SRC_DIR = _PROJECT_ROOT / "cli-wrapper" / "src" / "cli_wrapper"
|
|
14
|
+
if str(_SRC_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_SRC_DIR))
|
|
16
|
+
|
|
17
|
+
# Import the historic ``main`` function.
|
|
18
|
+
try:
|
|
19
|
+
from main import main as _original_main # type: ignore
|
|
20
|
+
except Exception as exc: # pragma: no cover – during early development the module may not be importable yet
|
|
21
|
+
raise ImportError("Unable to import legacy main function") from exc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
"""Entry point that forwards to the original implementation.
|
|
26
|
+
|
|
27
|
+
This wrapper exists so that external scripts that called the old ``main``
|
|
28
|
+
continue to operate. All behaviour (including interactive prompts) is
|
|
29
|
+
unchanged.
|
|
30
|
+
"""
|
|
31
|
+
_original_main()
|
bugbee/llm/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM provider for BugBee.
|
|
3
|
+
|
|
4
|
+
Currently BugBee uses DeepSeek-R1 through the Hugging Face Inference API.
|
|
5
|
+
|
|
6
|
+
Future versions can add OpenAI, Anthropic, Gemini, Ollama, etc.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
|
|
13
|
+
from bugbee.config.settings import settings
|
|
14
|
+
from bugbee.lazy import lazy_import
|
|
15
|
+
|
|
16
|
+
# Lazy imports (only loaded when the provider is created)
|
|
17
|
+
ChatHuggingFace = lazy_import("langchain_huggingface").ChatHuggingFace
|
|
18
|
+
HuggingFaceEndpoint = lazy_import("langchain_huggingface").HuggingFaceEndpoint
|
|
19
|
+
HumanMessage = lazy_import("langchain_core.messages").HumanMessage
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DeepSeekProvider:
|
|
23
|
+
"""DeepSeek provider backed by Hugging Face Inference API."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
|
|
27
|
+
self.endpoint = HuggingFaceEndpoint(
|
|
28
|
+
repo_id="deepseek-ai/DeepSeek-R1",
|
|
29
|
+
task="text-generation",
|
|
30
|
+
huggingfacehub_api_token=settings.huggingface_api_key,
|
|
31
|
+
temperature=settings.temperature,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self.model = ChatHuggingFace(
|
|
35
|
+
llm=self.endpoint,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def generate(self, prompt: str) -> str:
|
|
39
|
+
"""Generate a response from DeepSeek."""
|
|
40
|
+
|
|
41
|
+
response = self.model.invoke(
|
|
42
|
+
[
|
|
43
|
+
HumanMessage(
|
|
44
|
+
content=prompt
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return response.content
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@lru_cache(maxsize=1)
|
|
53
|
+
def default_provider() -> DeepSeekProvider:
|
|
54
|
+
"""
|
|
55
|
+
Singleton provider.
|
|
56
|
+
|
|
57
|
+
Prevents recreating the model every request.
|
|
58
|
+
"""
|
|
59
|
+
return DeepSeekProvider()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Metrics collection for BugBee.
|
|
2
|
+
|
|
3
|
+
The original script accumulated a dictionary ``metrics_obj`` and appended JSON
|
|
4
|
+
lines to ``metrics.jsonl``. ``MetricsCollector`` provides a thin wrapper around
|
|
5
|
+
that behaviour, exposing methods to update sections and write a record. All
|
|
6
|
+
timestamps are ISO‑8601 strings.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict
|
|
16
|
+
|
|
17
|
+
from bugbee.config.settings import settings
|
|
18
|
+
|
|
19
|
+
__all__ = ["MetricsCollector"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MetricsCollector:
|
|
23
|
+
"""Collect and persist metrics for a single run.
|
|
24
|
+
|
|
25
|
+
Usage example::
|
|
26
|
+
|
|
27
|
+
collector = MetricsCollector()
|
|
28
|
+
collector.update("command", "python myscript.py")
|
|
29
|
+
collector.update("retrieval", {"top_score": 0.9})
|
|
30
|
+
collector.write()
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, file_path: Path | None = None) -> None:
|
|
34
|
+
self.file_path = file_path or Path.cwd() / "metrics.jsonl"
|
|
35
|
+
self.metrics: Dict[str, Any] = {
|
|
36
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
37
|
+
"command": "",
|
|
38
|
+
"exceptionType": "NA",
|
|
39
|
+
"retrieval": {"top_score": 0.0, "avg_score": 0.0, "latency_ms": 0.0},
|
|
40
|
+
"llm": {"input_tokens": 0, "output_tokens": 0, "latency_ms": 0.0},
|
|
41
|
+
"diagnosis": {"error_line_found": False, "fix_generated": False},
|
|
42
|
+
"autofix": {"patch_applied": False, "files_modified": 0},
|
|
43
|
+
"validation": {"passed": False},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def update(self, key: str, value: Any) -> None:
|
|
47
|
+
"""Set *key* to *value* – creates nested dicts as needed."""
|
|
48
|
+
parts = key.split(".")
|
|
49
|
+
target = self.metrics
|
|
50
|
+
for part in parts[:-1]:
|
|
51
|
+
target = target.setdefault(part, {})
|
|
52
|
+
target[parts[-1]] = value
|
|
53
|
+
|
|
54
|
+
def write(self) -> None:
|
|
55
|
+
"""Append the current metrics as a JSON line to ``metrics.jsonl``."""
|
|
56
|
+
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
with self.file_path.open("a", encoding="utf-8") as f:
|
|
58
|
+
f.write(json.dumps(self.metrics) + "\n")
|
|
File without changes
|