bugbee 0.1.0__tar.gz → 0.1.3__tar.gz
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-0.1.0 → bugbee-0.1.3}/PKG-INFO +15 -15
- {bugbee-0.1.0 → bugbee-0.1.3}/README.md +14 -14
- bugbee-0.1.3/bugbee/cli/main.py +641 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/ui.py +5 -5
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee.egg-info/PKG-INFO +15 -15
- {bugbee-0.1.0 → bugbee-0.1.3}/pyproject.toml +1 -1
- bugbee-0.1.0/bugbee/cli/main.py +0 -258
- {bugbee-0.1.0 → bugbee-0.1.3}/LICENSE +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/cache/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/cli/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/config/settings.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/lazy.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/legacy.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/llm/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/metrics/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/parser/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/parser/error_parser.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/patcher/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/retrieval/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/utils/logging.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee/validator/__init__.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee.egg-info/SOURCES.txt +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee.egg-info/dependency_links.txt +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee.egg-info/entry_points.txt +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee.egg-info/requires.txt +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/bugbee.egg-info/top_level.txt +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/setup.cfg +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/tests/test.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/tests/test2.py +0 -0
- {bugbee-0.1.0 → bugbee-0.1.3}/tests/test_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bugbee
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: AI-powered CLI debugging and auto-repair tool with RAG, automatic patch generation, and validation.
|
|
5
5
|
Author-email: Pranav Kurmadasu <kurmadasu.pranav@example.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -46,7 +46,7 @@ Requires-Dist: build>=1.2.1; extra == "dev"
|
|
|
46
46
|
Requires-Dist: twine>=5.1.1; extra == "dev"
|
|
47
47
|
Dynamic: license-file
|
|
48
48
|
|
|
49
|
-
#
|
|
49
|
+
# BugBee
|
|
50
50
|
|
|
51
51
|
> **AI-powered CLI debugging assistant that automatically analyzes runtime errors, retrieves relevant documentation using RAG, and suggests or applies intelligent fixes.**
|
|
52
52
|
|
|
@@ -54,19 +54,19 @@ BugBee wraps your terminal commands, intercepts runtime failures, analyzes the t
|
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
57
|
-
##
|
|
58
|
-
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
* AI-powered debugging using **DeepSeek-R1**
|
|
60
|
+
* Retrieval-Augmented Generation (RAG) using **ChromaDB**
|
|
61
|
+
* Automatic traceback parsing
|
|
62
|
+
* Context-aware code analysis
|
|
63
|
+
* Retrieval from official framework documentation
|
|
64
|
+
* SHA256-based intelligent response caching
|
|
65
|
+
* Automatic code patch generation
|
|
66
|
+
* Validation after applying fixes
|
|
67
|
+
* Metrics collection
|
|
68
|
+
* Cross-platform support (Windows, Linux, macOS)
|
|
69
|
+
* Packaged for PyPI
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# BugBee
|
|
2
2
|
|
|
3
3
|
> **AI-powered CLI debugging assistant that automatically analyzes runtime errors, retrieves relevant documentation using RAG, and suggests or applies intelligent fixes.**
|
|
4
4
|
|
|
@@ -6,19 +6,19 @@ BugBee wraps your terminal commands, intercepts runtime failures, analyzes the t
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
* AI-powered debugging using **DeepSeek-R1**
|
|
12
|
+
* Retrieval-Augmented Generation (RAG) using **ChromaDB**
|
|
13
|
+
* Automatic traceback parsing
|
|
14
|
+
* Context-aware code analysis
|
|
15
|
+
* Retrieval from official framework documentation
|
|
16
|
+
* SHA256-based intelligent response caching
|
|
17
|
+
* Automatic code patch generation
|
|
18
|
+
* Validation after applying fixes
|
|
19
|
+
* Metrics collection
|
|
20
|
+
* Cross-platform support (Windows, Linux, macOS)
|
|
21
|
+
* Packaged for PyPI
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -0,0 +1,641 @@
|
|
|
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
|
+
def jayaho() -> None:
|
|
257
|
+
"""Displays a menu of machine learning topics and prints the corresponding code."""
|
|
258
|
+
print("Select an option:")
|
|
259
|
+
print("1. LinR")
|
|
260
|
+
print("2. Log")
|
|
261
|
+
print("3. KNNc")
|
|
262
|
+
print("4. KNNr")
|
|
263
|
+
print("5. SVM")
|
|
264
|
+
print("6. DTree.")
|
|
265
|
+
print("7. RForest.")
|
|
266
|
+
print("8. Naive")
|
|
267
|
+
print("9. K-means")
|
|
268
|
+
print("10. Hierarchical.")
|
|
269
|
+
print("11. CNN")
|
|
270
|
+
print("12. RNN")
|
|
271
|
+
print("13. LSTM")
|
|
272
|
+
print("14. ObjD")
|
|
273
|
+
print("15. Face")
|
|
274
|
+
|
|
275
|
+
choice = input("Enter your choice (1-15): ").strip()
|
|
276
|
+
|
|
277
|
+
if choice == "1":
|
|
278
|
+
print('''# Linear Regression
|
|
279
|
+
import numpy as np
|
|
280
|
+
import matplotlib.pyplot as plt
|
|
281
|
+
from sklearn.linear_model import LinearRegression
|
|
282
|
+
from sklearn.model_selection import train_test_split
|
|
283
|
+
from sklearn.metrics import mean_squared_error, r2_score
|
|
284
|
+
|
|
285
|
+
# Fixed dataset: area (sq. ft.) and price (lakhs)
|
|
286
|
+
X = np.array([500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400]).reshape(-1, 1)
|
|
287
|
+
y = np.array([15, 18, 21, 24, 27, 30, 33.9, 35.7, 39.6, 47])
|
|
288
|
+
|
|
289
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
290
|
+
X, y, test_size=0.2, random_state=42
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
model = LinearRegression()
|
|
294
|
+
model.fit(X_train, y_train)
|
|
295
|
+
|
|
296
|
+
pred = model.predict(X_test)
|
|
297
|
+
print("R2 Score:", r2_score(y_test, pred))
|
|
298
|
+
print("MSE:", mean_squared_error(y_test, pred))
|
|
299
|
+
|
|
300
|
+
plt.scatter(X, y, label="Actual data")
|
|
301
|
+
plt.plot(X, model.predict(X), color="red", label="Regression line")
|
|
302
|
+
plt.xlabel("Area (sq. ft.)")
|
|
303
|
+
plt.ylabel("Price (lakhs)")
|
|
304
|
+
plt.legend()
|
|
305
|
+
plt.show()
|
|
306
|
+
|
|
307
|
+
print("Price for 1500 sq.ft.:", model.predict([[1500]])[0])''')
|
|
308
|
+
elif choice == "2":
|
|
309
|
+
print('''# Logistic Regression
|
|
310
|
+
from sklearn.datasets import load_iris
|
|
311
|
+
from sklearn.model_selection import train_test_split
|
|
312
|
+
from sklearn.linear_model import LogisticRegression
|
|
313
|
+
from sklearn.metrics import accuracy_score, classification_report
|
|
314
|
+
|
|
315
|
+
data = load_iris()
|
|
316
|
+
X, y = data.data, data.target
|
|
317
|
+
|
|
318
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
319
|
+
X, y, test_size=0.2, random_state=42, stratify=y
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
model = LogisticRegression(max_iter=200)
|
|
323
|
+
model.fit(X_train, y_train)
|
|
324
|
+
|
|
325
|
+
pred = model.predict(X_test)
|
|
326
|
+
print("Accuracy:", accuracy_score(y_test, pred))
|
|
327
|
+
print(classification_report(y_test, pred, target_names=data.target_names))
|
|
328
|
+
|
|
329
|
+
# Predict a new flower
|
|
330
|
+
sample = [[5.1, 3.5, 1.4, 0.2]]
|
|
331
|
+
print("Predicted flower:", data.target_names[model.predict(sample)[0]])''')
|
|
332
|
+
elif choice == "3":
|
|
333
|
+
print('''# KNN Classification
|
|
334
|
+
from sklearn.datasets import load_iris
|
|
335
|
+
from sklearn.model_selection import train_test_split
|
|
336
|
+
from sklearn.neighbors import KNeighborsClassifier
|
|
337
|
+
from sklearn.metrics import accuracy_score, classification_report
|
|
338
|
+
from sklearn.preprocessing import StandardScaler
|
|
339
|
+
from sklearn.pipeline import make_pipeline
|
|
340
|
+
|
|
341
|
+
data = load_iris()
|
|
342
|
+
X, y = data.data, data.target
|
|
343
|
+
|
|
344
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
345
|
+
X, y, test_size=0.2, random_state=42, stratify=y
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
model = KNeighborsClassifier(n_neighbors=3)
|
|
349
|
+
model.fit(X_train, y_train)
|
|
350
|
+
|
|
351
|
+
pred = model.predict(X_test)
|
|
352
|
+
print("Accuracy:", accuracy_score(y_test, pred))
|
|
353
|
+
print(classification_report(y_test, pred, target_names=data.target_names))
|
|
354
|
+
|
|
355
|
+
print("New flower:", data.target_names[
|
|
356
|
+
model.predict([[5.1, 3.5, 1.4, 0.2]])[0]
|
|
357
|
+
])''')
|
|
358
|
+
elif choice == "4":
|
|
359
|
+
print('''# KNN Regression
|
|
360
|
+
import numpy as np
|
|
361
|
+
import matplotlib.pyplot as plt
|
|
362
|
+
from sklearn.neighbors import KNeighborsRegressor
|
|
363
|
+
from sklearn.model_selection import train_test_split
|
|
364
|
+
from sklearn.metrics import mean_squared_error, r2_score
|
|
365
|
+
|
|
366
|
+
X = np.array([500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400]).reshape(-1, 1)
|
|
367
|
+
y = np.array([15, 18, 21, 24, 27, 30, 33, 36, 39, 42])
|
|
368
|
+
|
|
369
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
370
|
+
X, y, test_size=0.5, random_state=42
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
model = KNeighborsRegressor(n_neighbors=3)
|
|
374
|
+
model.fit(X_train, y_train)
|
|
375
|
+
|
|
376
|
+
pred = model.predict(X_test)
|
|
377
|
+
print("R2 Score:", r2_score(y_test, pred))
|
|
378
|
+
print("MSE:", mean_squared_error(y_test, pred))
|
|
379
|
+
print("Price for 950 sq.ft.:", model.predict([[950]])[0])''')
|
|
380
|
+
elif choice == "5":
|
|
381
|
+
print('''# SVM
|
|
382
|
+
from sklearn.datasets import load_iris
|
|
383
|
+
from sklearn.model_selection import train_test_split
|
|
384
|
+
from sklearn.svm import SVC
|
|
385
|
+
from sklearn.metrics import accuracy_score, classification_report
|
|
386
|
+
from sklearn.pipeline import make_pipeline
|
|
387
|
+
from sklearn.preprocessing import StandardScaler
|
|
388
|
+
|
|
389
|
+
data = load_iris()
|
|
390
|
+
X, y = data.data, data.target
|
|
391
|
+
|
|
392
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
393
|
+
X, y, test_size=0.2, random_state=42, stratify=y
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
model = SVC(kernel="linear")
|
|
397
|
+
model.fit(X_train, y_train)
|
|
398
|
+
|
|
399
|
+
pred = model.predict(X_test)
|
|
400
|
+
print("Accuracy:", accuracy_score(y_test, pred))
|
|
401
|
+
print(classification_report(y_test, pred, target_names=data.target_names))
|
|
402
|
+
|
|
403
|
+
print("New flower:", data.target_names[
|
|
404
|
+
model.predict([[5.2, 3.5, 1.4, 0.2]])[0]
|
|
405
|
+
])''')
|
|
406
|
+
elif choice == "6":
|
|
407
|
+
print('''# Decision Tree
|
|
408
|
+
from sklearn.datasets import load_iris
|
|
409
|
+
from sklearn.model_selection import train_test_split
|
|
410
|
+
from sklearn.tree import DecisionTreeClassifier
|
|
411
|
+
from sklearn.metrics import accuracy_score, classification_report
|
|
412
|
+
|
|
413
|
+
data = load_iris()
|
|
414
|
+
X, y = data.data, data.target
|
|
415
|
+
|
|
416
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
417
|
+
X, y, test_size=0.2, random_state=42, stratify=y
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
model = DecisionTreeClassifier(max_depth=3, random_state=42)
|
|
421
|
+
model.fit(X_train, y_train)
|
|
422
|
+
|
|
423
|
+
pred = model.predict(X_test)
|
|
424
|
+
print("Accuracy:", accuracy_score(y_test, pred))
|
|
425
|
+
print(classification_report(y_test, pred, target_names=data.target_names))
|
|
426
|
+
|
|
427
|
+
print("New flower:", data.target_names[
|
|
428
|
+
model.predict([[5.1, 3.5, 1.4, 0.2]])[0]
|
|
429
|
+
])''')
|
|
430
|
+
elif choice == "7":
|
|
431
|
+
print('''# Random Forest
|
|
432
|
+
from sklearn.datasets import load_iris
|
|
433
|
+
from sklearn.model_selection import train_test_split
|
|
434
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
435
|
+
from sklearn.metrics import accuracy_score, classification_report
|
|
436
|
+
|
|
437
|
+
data = load_iris()
|
|
438
|
+
X, y = data.data, data.target
|
|
439
|
+
|
|
440
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
441
|
+
X, y, test_size=0.2, random_state=42, stratify=y
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
model = RandomForestClassifier(n_estimators=100, random_state=42)
|
|
445
|
+
model.fit(X_train, y_train)
|
|
446
|
+
|
|
447
|
+
pred = model.predict(X_test)
|
|
448
|
+
print("Accuracy:", accuracy_score(y_test, pred))
|
|
449
|
+
print(classification_report(y_test, pred, target_names=data.target_names))
|
|
450
|
+
|
|
451
|
+
print("New flower:", data.target_names[
|
|
452
|
+
model.predict([[5.1, 3.5, 1.4, 0.2]])[0]
|
|
453
|
+
])''')
|
|
454
|
+
elif choice == "8":
|
|
455
|
+
print('''# Naive Bayes
|
|
456
|
+
from sklearn.datasets import load_iris
|
|
457
|
+
from sklearn.model_selection import train_test_split
|
|
458
|
+
from sklearn.naive_bayes import GaussianNB
|
|
459
|
+
from sklearn.metrics import accuracy_score, classification_report
|
|
460
|
+
|
|
461
|
+
data = load_iris()
|
|
462
|
+
X, y = data.data, data.target
|
|
463
|
+
|
|
464
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
465
|
+
X, y, test_size=0.2, random_state=42, stratify=y
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
model = GaussianNB()
|
|
469
|
+
model.fit(X_train, y_train)
|
|
470
|
+
|
|
471
|
+
pred = model.predict(X_test)
|
|
472
|
+
print("Accuracy:", accuracy_score(y_test, pred))
|
|
473
|
+
print(classification_report(y_test, pred, target_names=data.target_names))
|
|
474
|
+
|
|
475
|
+
print("New flower:", data.target_names[
|
|
476
|
+
model.predict([[5.1, 3.5, 1.4, 0.2]])[0]
|
|
477
|
+
])''')
|
|
478
|
+
elif choice == "9":
|
|
479
|
+
print('''# K-Means
|
|
480
|
+
import matplotlib.pyplot as plt
|
|
481
|
+
from sklearn.datasets import load_iris
|
|
482
|
+
from sklearn.cluster import KMeans
|
|
483
|
+
from sklearn.preprocessing import StandardScaler
|
|
484
|
+
from sklearn.metrics import silhouette_score
|
|
485
|
+
|
|
486
|
+
data = load_iris()
|
|
487
|
+
X = data.data[:, :2] # First two features for easy plotting
|
|
488
|
+
X = StandardScaler().fit_transform(X)
|
|
489
|
+
|
|
490
|
+
# K-Means
|
|
491
|
+
km = KMeans(n_clusters=3, random_state=42, n_init=10)
|
|
492
|
+
labels_km = km.fit_predict(X)
|
|
493
|
+
|
|
494
|
+
print("K-Means Silhouette:", silhouette_score(X, labels_km))
|
|
495
|
+
|
|
496
|
+
plt.scatter(X[:, 0], X[:, 1], c=labels_km)
|
|
497
|
+
plt.title("K-Means Clustering")
|
|
498
|
+
plt.xlabel("Sepal length (scaled)")
|
|
499
|
+
plt.ylabel("Sepal width (scaled)")
|
|
500
|
+
plt.show()''')
|
|
501
|
+
elif choice == "10":
|
|
502
|
+
print('''# Hierarchical Clustering
|
|
503
|
+
import matplotlib.pyplot as plt
|
|
504
|
+
from sklearn.datasets import load_iris
|
|
505
|
+
from sklearn.cluster import AgglomerativeClustering
|
|
506
|
+
from sklearn.preprocessing import StandardScaler
|
|
507
|
+
from sklearn.metrics import silhouette_score
|
|
508
|
+
|
|
509
|
+
data = load_iris()
|
|
510
|
+
X = data.data[:, :2] # First two features for easy plotting
|
|
511
|
+
X = StandardScaler().fit_transform(X)
|
|
512
|
+
|
|
513
|
+
# Hierarchical clustering
|
|
514
|
+
hc = AgglomerativeClustering(n_clusters=3, linkage="ward")
|
|
515
|
+
labels_hc = hc.fit_predict(X)
|
|
516
|
+
|
|
517
|
+
print("Hierarchical Silhouette:", silhouette_score(X, labels_hc))
|
|
518
|
+
|
|
519
|
+
plt.scatter(X[:, 0], X[:, 1], c=labels_hc)
|
|
520
|
+
plt.title("Hierarchical Clustering")
|
|
521
|
+
plt.xlabel("Sepal length (scaled)")
|
|
522
|
+
plt.ylabel("Sepal width (scaled)")
|
|
523
|
+
plt.show()''')
|
|
524
|
+
elif choice == "11":
|
|
525
|
+
print('''# CNN
|
|
526
|
+
import numpy as np
|
|
527
|
+
import matplotlib.pyplot as plt
|
|
528
|
+
import tensorflow as tf
|
|
529
|
+
from tensorflow.keras import layers, models
|
|
530
|
+
|
|
531
|
+
# Load fixed built-in dataset
|
|
532
|
+
(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()
|
|
533
|
+
|
|
534
|
+
# Normalize pixels and add channel dimension
|
|
535
|
+
X_train = X_train.astype("float32") / 255.0
|
|
536
|
+
X_test = X_test.astype("float32") / 255.0
|
|
537
|
+
X_train = X_train[..., np.newaxis]
|
|
538
|
+
X_test = X_test[..., np.newaxis]
|
|
539
|
+
|
|
540
|
+
# CNN model
|
|
541
|
+
model = models.Sequential([
|
|
542
|
+
layers.Input(shape=(28, 28, 1)),
|
|
543
|
+
layers.Conv2D(16, (3, 3), activation="relu"),
|
|
544
|
+
layers.MaxPooling2D((2, 2)),
|
|
545
|
+
layers.Flatten(),
|
|
546
|
+
layers.Dense(64, activation="relu"),
|
|
547
|
+
layers.Dense(10, activation="softmax")
|
|
548
|
+
])
|
|
549
|
+
|
|
550
|
+
model.compile(optimizer="adam",
|
|
551
|
+
loss="sparse_categorical_crossentropy",
|
|
552
|
+
metrics=["accuracy"])
|
|
553
|
+
|
|
554
|
+
model.fit(X_train, y_train, epochs=3, batch_size=64, validation_split=0.1, verbose=1)
|
|
555
|
+
|
|
556
|
+
loss, acc = model.evaluate(X_test, y_test, verbose=0)
|
|
557
|
+
print("Test accuracy:", acc)
|
|
558
|
+
|
|
559
|
+
# Give an input by choosing a test-image index
|
|
560
|
+
i = int(input("Enter test image index (0-9999): "))
|
|
561
|
+
i = max(0, min(i, len(X_test) - 1))
|
|
562
|
+
prediction = model.predict(X_test[i:i+1], verbose=0)
|
|
563
|
+
print("Predicted digit:", np.argmax(prediction))
|
|
564
|
+
print("Actual digit:", y_test[i])
|
|
565
|
+
|
|
566
|
+
plt.imshow(X_test[i].squeeze(), cmap="gray")
|
|
567
|
+
plt.title("Predicted: " + str(np.argmax(prediction)))
|
|
568
|
+
plt.axis("off")
|
|
569
|
+
plt.show()''')
|
|
570
|
+
elif choice in ["12", "13"]:
|
|
571
|
+
print('''# RNN and LSTM
|
|
572
|
+
import numpy as np
|
|
573
|
+
import matplotlib.pyplot as plt
|
|
574
|
+
import tensorflow as tf
|
|
575
|
+
from tensorflow.keras import Sequential
|
|
576
|
+
from tensorflow.keras.layers import SimpleRNN, LSTM, Dense
|
|
577
|
+
|
|
578
|
+
# Fixed time-series data
|
|
579
|
+
data = np.array([
|
|
580
|
+
10, 12, 13, 15, 14, 16, 18, 17, 20, 21,
|
|
581
|
+
23, 22, 25, 27, 26, 29, 30, 32, 31, 34,
|
|
582
|
+
36, 35, 38, 40, 39, 42, 44, 43, 46, 48
|
|
583
|
+
], dtype="float32")
|
|
584
|
+
|
|
585
|
+
# Create sequences of 3 values to predict the next value
|
|
586
|
+
X, y = [], []
|
|
587
|
+
for i in range(len(data) - 3):
|
|
588
|
+
X.append(data[i:i+3])
|
|
589
|
+
y.append(data[i+3])
|
|
590
|
+
X = np.array(X).reshape(-1, 3, 1)
|
|
591
|
+
y = np.array(y)
|
|
592
|
+
|
|
593
|
+
# Chronological split: first 80% train, last 20% test
|
|
594
|
+
split = int(0.8 * len(X))
|
|
595
|
+
X_train, X_test = X[:split], X[split:]
|
|
596
|
+
y_train, y_test = y[:split], y[split:]
|
|
597
|
+
|
|
598
|
+
# RNN
|
|
599
|
+
rnn = Sequential([
|
|
600
|
+
tf.keras.Input(shape=(3, 1)),
|
|
601
|
+
SimpleRNN(16, activation="tanh"),
|
|
602
|
+
Dense(1)
|
|
603
|
+
])
|
|
604
|
+
rnn.compile(optimizer="adam", loss="mse")
|
|
605
|
+
rnn.fit(X_train, y_train, epochs=100, verbose=0)
|
|
606
|
+
|
|
607
|
+
# LSTM
|
|
608
|
+
lstm = Sequential([
|
|
609
|
+
tf.keras.Input(shape=(3, 1)),
|
|
610
|
+
LSTM(16),
|
|
611
|
+
Dense(1)
|
|
612
|
+
])
|
|
613
|
+
lstm.compile(optimizer="adam", loss="mse")
|
|
614
|
+
lstm.fit(X_train, y_train, epochs=100, verbose=0)
|
|
615
|
+
|
|
616
|
+
# Predictions
|
|
617
|
+
rnn_pred = rnn.predict(X_test, verbose=0).flatten()
|
|
618
|
+
lstm_pred = lstm.predict(X_test, verbose=0).flatten()
|
|
619
|
+
|
|
620
|
+
print("RNN Test MSE:", np.mean((y_test - rnn_pred) ** 2))
|
|
621
|
+
print("LSTM Test MSE:", np.mean((y_test - lstm_pred) ** 2))
|
|
622
|
+
|
|
623
|
+
plt.plot(y_test, marker="o", label="Actual")
|
|
624
|
+
plt.plot(rnn_pred, marker="x", label="RNN")
|
|
625
|
+
plt.plot(lstm_pred, marker="s", label="LSTM")
|
|
626
|
+
plt.title("Time-Series Prediction")
|
|
627
|
+
plt.xlabel("Test time step")
|
|
628
|
+
plt.ylabel("Value")
|
|
629
|
+
plt.legend()
|
|
630
|
+
plt.show()
|
|
631
|
+
|
|
632
|
+
# Predict the next value from your own three values
|
|
633
|
+
sample = np.array([40, 42, 44], dtype="float32").reshape(1, 3, 1)
|
|
634
|
+
print("RNN next value:", rnn.predict(sample, verbose=0)[0][0])
|
|
635
|
+
print("LSTM next value:", lstm.predict(sample, verbose=0)[0][0])''')
|
|
636
|
+
elif choice in ["14", "15"]:
|
|
637
|
+
pass # Code not available in pdf
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
if __name__ == "__main__":
|
|
641
|
+
app()
|
|
@@ -117,17 +117,17 @@ class UI:
|
|
|
117
117
|
def location_found(self, file_path: str, line_number: int) -> None:
|
|
118
118
|
_log.debug(f"Extracted location: {file_path}:{line_number}")
|
|
119
119
|
self.console.print(
|
|
120
|
-
f"[bugbee.info]
|
|
120
|
+
f"[bugbee.info]Located error at[/] [bugbee.path]{file_path}:{line_number}[/]"
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
def location_not_found(self) -> None:
|
|
124
124
|
_log.debug("Could not determine source location from stderr.")
|
|
125
|
-
self.console.print("[bugbee.warning]
|
|
125
|
+
self.console.print("[bugbee.warning]Could not determine the source location.[/]")
|
|
126
126
|
|
|
127
127
|
def cache_hit(self) -> None:
|
|
128
128
|
_log.info("Cache hit – using previously generated response.")
|
|
129
129
|
self.console.print(
|
|
130
|
-
"[bugbee.info]
|
|
130
|
+
"[bugbee.info]Cache hit[/] [bugbee.muted]— reusing a previous analysis.[/]"
|
|
131
131
|
)
|
|
132
132
|
|
|
133
133
|
def cache_miss(self) -> None:
|
|
@@ -136,7 +136,7 @@ class UI:
|
|
|
136
136
|
|
|
137
137
|
def cache_cleared(self) -> None:
|
|
138
138
|
_log.info("Cache cleared.")
|
|
139
|
-
self.console.print("[bugbee.success]
|
|
139
|
+
self.console.print("[bugbee.success]Cache cleared.[/]")
|
|
140
140
|
|
|
141
141
|
def retrieval_stats(self, top_score: float, avg_score: float) -> None:
|
|
142
142
|
_log.debug(f"Retrieval scores — top: {top_score}, avg: {avg_score}")
|
|
@@ -146,7 +146,7 @@ class UI:
|
|
|
146
146
|
# ------------------------------------------------------------------
|
|
147
147
|
def show_analysis(self, content: str) -> None:
|
|
148
148
|
self.console.print(
|
|
149
|
-
Panel(content, title="
|
|
149
|
+
Panel(content, title="AI Analysis", border_style="cyan", expand=True)
|
|
150
150
|
)
|
|
151
151
|
|
|
152
152
|
# ------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bugbee
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: AI-powered CLI debugging and auto-repair tool with RAG, automatic patch generation, and validation.
|
|
5
5
|
Author-email: Pranav Kurmadasu <kurmadasu.pranav@example.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -46,7 +46,7 @@ Requires-Dist: build>=1.2.1; extra == "dev"
|
|
|
46
46
|
Requires-Dist: twine>=5.1.1; extra == "dev"
|
|
47
47
|
Dynamic: license-file
|
|
48
48
|
|
|
49
|
-
#
|
|
49
|
+
# BugBee
|
|
50
50
|
|
|
51
51
|
> **AI-powered CLI debugging assistant that automatically analyzes runtime errors, retrieves relevant documentation using RAG, and suggests or applies intelligent fixes.**
|
|
52
52
|
|
|
@@ -54,19 +54,19 @@ BugBee wraps your terminal commands, intercepts runtime failures, analyzes the t
|
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
57
|
-
##
|
|
58
|
-
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
* AI-powered debugging using **DeepSeek-R1**
|
|
60
|
+
* Retrieval-Augmented Generation (RAG) using **ChromaDB**
|
|
61
|
+
* Automatic traceback parsing
|
|
62
|
+
* Context-aware code analysis
|
|
63
|
+
* Retrieval from official framework documentation
|
|
64
|
+
* SHA256-based intelligent response caching
|
|
65
|
+
* Automatic code patch generation
|
|
66
|
+
* Validation after applying fixes
|
|
67
|
+
* Metrics collection
|
|
68
|
+
* Cross-platform support (Windows, Linux, macOS)
|
|
69
|
+
* Packaged for PyPI
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "bugbee"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "AI-powered CLI debugging and auto-repair tool with RAG, automatic patch generation, and validation."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11,<3.13"
|
bugbee-0.1.0/bugbee/cli/main.py
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|