reducio 0.1.1__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.
- reducio/__init__.py +3 -0
- reducio/__main__.py +4 -0
- reducio/agents/__init__.py +17 -0
- reducio/agents/analyzer.py +17 -0
- reducio/agents/base.py +151 -0
- reducio/agents/deduplicator.py +193 -0
- reducio/agents/idiomatizer.py +85 -0
- reducio/agents/pattern.py +225 -0
- reducio/agents/quality_checker.py +204 -0
- reducio/analysis.py +144 -0
- reducio/annotations.py +30 -0
- reducio/cli.py +741 -0
- reducio/compare.py +178 -0
- reducio/config.py +95 -0
- reducio/embeddings/__init__.py +16 -0
- reducio/embeddings/service.py +88 -0
- reducio/git_safety.py +42 -0
- reducio/history.py +229 -0
- reducio/history_report.py +114 -0
- reducio/history_view.js +117 -0
- reducio/idioms.py +422 -0
- reducio/llm/__init__.py +5 -0
- reducio/llm/router.py +118 -0
- reducio/metrics.py +218 -0
- reducio/models.py +355 -0
- reducio/parse.py +38 -0
- reducio/plan_review.py +131 -0
- reducio/presentation.py +40 -0
- reducio/progress.py +69 -0
- reducio/quality_gate.py +22 -0
- reducio/recovery.py +140 -0
- reducio/repo.py +130 -0
- reducio/reporter.py +208 -0
- reducio/runner.py +102 -0
- reducio/services.py +216 -0
- reducio/session.py +272 -0
- reducio/storage.py +64 -0
- reducio/utils/__init__.py +12 -0
- reducio/utils/code_utils.py +21 -0
- reducio/visual_report.py +515 -0
- reducio/workspace.py +159 -0
- reducio-0.1.1.dist-info/METADATA +94 -0
- reducio-0.1.1.dist-info/RECORD +46 -0
- reducio-0.1.1.dist-info/WHEEL +4 -0
- reducio-0.1.1.dist-info/entry_points.txt +2 -0
- reducio-0.1.1.dist-info/licenses/LICENSE +21 -0
reducio/__init__.py
ADDED
reducio/__main__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent implementations for code analysis and refactoring.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from reducio.agents.analyzer import AnalyzerAgent
|
|
6
|
+
from reducio.agents.deduplicator import DeduplicatorAgent
|
|
7
|
+
from reducio.agents.idiomatizer import IdiomatizerAgent
|
|
8
|
+
from reducio.agents.pattern import PatternAgent
|
|
9
|
+
from reducio.agents.quality_checker import QualityCheckerAgent
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AnalyzerAgent",
|
|
13
|
+
"DeduplicatorAgent",
|
|
14
|
+
"IdiomatizerAgent",
|
|
15
|
+
"PatternAgent",
|
|
16
|
+
"QualityCheckerAgent",
|
|
17
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Analyzer agent using the shared, versioned source metrics."""
|
|
2
|
+
|
|
3
|
+
from reducio.analysis import analyze_files
|
|
4
|
+
from reducio.models import AnalyzeRequest, AnalyzeResult
|
|
5
|
+
from reducio.workspace import Workspace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AnalyzerAgent:
|
|
9
|
+
def __init__(self, workspace: Workspace):
|
|
10
|
+
self.workspace = workspace
|
|
11
|
+
|
|
12
|
+
async def analyze(self, request: AnalyzeRequest) -> AnalyzeResult:
|
|
13
|
+
return analyze_files(
|
|
14
|
+
request.files or self.workspace.list_files(),
|
|
15
|
+
self.workspace.cfg,
|
|
16
|
+
request.path,
|
|
17
|
+
)
|
reducio/agents/base.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Base agent class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from reducio.models import FileChange, PlanDiagnostic, PlanningProvenance, RefactorPlan
|
|
9
|
+
from reducio.plan_review import validate_plan
|
|
10
|
+
from reducio.session import SessionStore
|
|
11
|
+
from reducio.utils.code_utils import strip_code_fence
|
|
12
|
+
from reducio.workspace import Workspace
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from reducio.llm.router import LLMClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ModelRewriteError(RuntimeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseAgent:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
workspace: Workspace | None = None,
|
|
26
|
+
llm_router: LLMClient | None = None,
|
|
27
|
+
session_store: SessionStore | None = None,
|
|
28
|
+
):
|
|
29
|
+
self.workspace = workspace
|
|
30
|
+
self.llm = llm_router
|
|
31
|
+
self.session_store = session_store or SessionStore(
|
|
32
|
+
str(workspace.root / ".reducio" / "sessions") if workspace else ".reducio/sessions"
|
|
33
|
+
)
|
|
34
|
+
self._begin_plan()
|
|
35
|
+
|
|
36
|
+
def _begin_plan(self, allow_fallback: bool = False) -> None:
|
|
37
|
+
self.allow_fallback = allow_fallback
|
|
38
|
+
self.diagnostics: list[PlanDiagnostic] = []
|
|
39
|
+
self.provenance: list[PlanningProvenance] = []
|
|
40
|
+
|
|
41
|
+
def _generate_session_id(self) -> str:
|
|
42
|
+
return str(uuid.uuid4())
|
|
43
|
+
|
|
44
|
+
def _save_plan(self, plan: RefactorPlan, command_type: str) -> None:
|
|
45
|
+
self.session_store.save_plan(plan, command_type=command_type)
|
|
46
|
+
|
|
47
|
+
def get_plan(self, session_id: str) -> RefactorPlan | None:
|
|
48
|
+
return self.session_store.load_plan(session_id)
|
|
49
|
+
|
|
50
|
+
def _file_content_path(self, file) -> tuple[str, str]:
|
|
51
|
+
if hasattr(file, "content"):
|
|
52
|
+
return file.content, file.path
|
|
53
|
+
return file["content"], file["path"]
|
|
54
|
+
|
|
55
|
+
def _llm_enabled(self) -> bool:
|
|
56
|
+
# An explicit model request must not silently degrade when no router is wired.
|
|
57
|
+
return bool(
|
|
58
|
+
self.workspace
|
|
59
|
+
and (
|
|
60
|
+
self.workspace.cfg.model
|
|
61
|
+
or self.workspace.cfg.llm_api
|
|
62
|
+
or self.workspace.cfg.llm_base_url
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def _llm_rewrite(
|
|
67
|
+
self, content: str, path: str, instruction: str, description: str
|
|
68
|
+
) -> FileChange | None:
|
|
69
|
+
"""Ask the LLM to rewrite a whole module; returns a reviewable change or None."""
|
|
70
|
+
prompt = (
|
|
71
|
+
f"{instruction}\nReturn ONLY the complete rewritten module, no prose.\n\n"
|
|
72
|
+
f"```python\n{content}\n```"
|
|
73
|
+
)
|
|
74
|
+
model = self.workspace.cfg.model if self.workspace else ""
|
|
75
|
+
model = "custom endpoint" if "://" in model else model
|
|
76
|
+
try:
|
|
77
|
+
if self.llm is None:
|
|
78
|
+
raise ModelRewriteError("No model router available")
|
|
79
|
+
raw = await self.llm.complete(
|
|
80
|
+
prompt, system_prompt="You are an expert Python engineer."
|
|
81
|
+
)
|
|
82
|
+
code = strip_code_fence(raw)
|
|
83
|
+
if not code.strip():
|
|
84
|
+
raise ValueError("Empty model response")
|
|
85
|
+
compile(code, path, "exec")
|
|
86
|
+
except Exception as error:
|
|
87
|
+
from reducio.llm.router import LLMError
|
|
88
|
+
|
|
89
|
+
detail = f": {error}" if isinstance(error, LLMError) else ""
|
|
90
|
+
self.diagnostics.append(
|
|
91
|
+
PlanDiagnostic(
|
|
92
|
+
code="model_failed",
|
|
93
|
+
file=path,
|
|
94
|
+
message="Model rewrite failed or returned empty/invalid Python"
|
|
95
|
+
+ detail
|
|
96
|
+
+ (
|
|
97
|
+
"; explicit fallback enabled"
|
|
98
|
+
if self.allow_fallback
|
|
99
|
+
else "; no application permitted"
|
|
100
|
+
),
|
|
101
|
+
severity="warning" if self.allow_fallback else "error",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
self.provenance.append(
|
|
105
|
+
PlanningProvenance(file=path, engine="model", outcome="failed", model=model)
|
|
106
|
+
)
|
|
107
|
+
raise ModelRewriteError("Model rewrite unavailable") from None
|
|
108
|
+
self.provenance.append(
|
|
109
|
+
PlanningProvenance(
|
|
110
|
+
file=path,
|
|
111
|
+
engine="model",
|
|
112
|
+
outcome="unchanged" if code.strip() == content.strip() else "proposed",
|
|
113
|
+
model=model,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
if code.strip() == content.strip():
|
|
117
|
+
return None
|
|
118
|
+
return FileChange(
|
|
119
|
+
path=path,
|
|
120
|
+
original=content,
|
|
121
|
+
modified=code if code.endswith("\n") else code + "\n",
|
|
122
|
+
description=description,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _finalize_plan(
|
|
126
|
+
self, changes: list, description: str, command_type: str, **plan_kw
|
|
127
|
+
) -> RefactorPlan:
|
|
128
|
+
plan = RefactorPlan(
|
|
129
|
+
schema_version=2,
|
|
130
|
+
session_id=self._generate_session_id(),
|
|
131
|
+
changes=changes,
|
|
132
|
+
description=description,
|
|
133
|
+
diagnostics=self.diagnostics,
|
|
134
|
+
provenance=self.provenance,
|
|
135
|
+
**plan_kw,
|
|
136
|
+
)
|
|
137
|
+
for change in plan.changes:
|
|
138
|
+
if change.operation is None:
|
|
139
|
+
change.operation = "create" if change.creates_file else "replace"
|
|
140
|
+
# Coalesce truly identical changes, but never silently pick one conflicting version.
|
|
141
|
+
unique = []
|
|
142
|
+
for change in plan.changes:
|
|
143
|
+
if change not in unique:
|
|
144
|
+
unique.append(change)
|
|
145
|
+
plan.changes = unique
|
|
146
|
+
plan.diagnostics.extend(
|
|
147
|
+
validate_plan(plan, self.workspace.root if self.workspace else None)
|
|
148
|
+
)
|
|
149
|
+
plan.complete = not any(item.severity == "error" for item in plan.diagnostics)
|
|
150
|
+
self._save_plan(plan, command_type)
|
|
151
|
+
return plan
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Deduplicator agent — semantic duplicate detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import builtins
|
|
7
|
+
import symtable
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from reducio.agents.base import BaseAgent
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from reducio.embeddings.service import EmbeddingService
|
|
14
|
+
from reducio.models import (
|
|
15
|
+
CodeBlock,
|
|
16
|
+
DeduplicateRequest,
|
|
17
|
+
FileChange,
|
|
18
|
+
FileInfo,
|
|
19
|
+
Language,
|
|
20
|
+
PlanDiagnostic,
|
|
21
|
+
PlanningProvenance,
|
|
22
|
+
RefactorPlan,
|
|
23
|
+
)
|
|
24
|
+
from reducio.parse import ParserError, get_complexity
|
|
25
|
+
from reducio.plan_review import advisory_path
|
|
26
|
+
from reducio.repo import detect_language
|
|
27
|
+
from reducio.session import SessionStore
|
|
28
|
+
from reducio.workspace import Workspace
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DeduplicatorAgent(BaseAgent):
|
|
32
|
+
workspace: Workspace
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
workspace: Workspace,
|
|
37
|
+
embedding_service: EmbeddingService,
|
|
38
|
+
llm_router=None,
|
|
39
|
+
session_store: SessionStore | None = None,
|
|
40
|
+
):
|
|
41
|
+
super().__init__(workspace, llm_router, session_store)
|
|
42
|
+
self.embedding_service = embedding_service
|
|
43
|
+
|
|
44
|
+
async def find_duplicates(self, request: DeduplicateRequest) -> RefactorPlan:
|
|
45
|
+
self._begin_plan()
|
|
46
|
+
files = request.files or self.workspace.list_files()
|
|
47
|
+
if not self.embedding_service.is_using_real_embeddings:
|
|
48
|
+
self.diagnostics.append(
|
|
49
|
+
PlanDiagnostic(
|
|
50
|
+
code="embeddings_unavailable",
|
|
51
|
+
severity="error",
|
|
52
|
+
message="Semantic embeddings unavailable; install reducio[embeddings] and retry.",
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return self._finalize_plan(
|
|
56
|
+
[],
|
|
57
|
+
"Semantic embeddings unavailable; no duplicates analyzed. See installation docs for the embeddings extra.",
|
|
58
|
+
"deduplicate",
|
|
59
|
+
)
|
|
60
|
+
blocks = self._extract_blocks(files)
|
|
61
|
+
try:
|
|
62
|
+
groups = await self.embedding_service.find_duplicates(
|
|
63
|
+
blocks, request.similarity_threshold
|
|
64
|
+
)
|
|
65
|
+
except Exception:
|
|
66
|
+
self.diagnostics.append(
|
|
67
|
+
PlanDiagnostic(
|
|
68
|
+
code="embeddings_failed",
|
|
69
|
+
severity="error",
|
|
70
|
+
message="Semantic duplicate search failed; no application permitted.",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
groups = []
|
|
74
|
+
self.provenance.append(
|
|
75
|
+
PlanningProvenance(
|
|
76
|
+
file="",
|
|
77
|
+
engine="embeddings",
|
|
78
|
+
outcome=(
|
|
79
|
+
"failed" if any(d.severity == "error" for d in self.diagnostics) else "scanned"
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
changes = []
|
|
84
|
+
for group in groups:
|
|
85
|
+
if len(group) >= 2:
|
|
86
|
+
ch = self._create_dedup_change(group)
|
|
87
|
+
if ch:
|
|
88
|
+
changes.append(ch)
|
|
89
|
+
return self._finalize_plan(
|
|
90
|
+
changes,
|
|
91
|
+
f"Found {len(groups)} duplicate group(s); proposing {len(changes)} shared-util "
|
|
92
|
+
"suggestion(s) (review before adopting — call sites are not rewritten).",
|
|
93
|
+
"deduplicate",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _extract_blocks(self, files: list[FileInfo]) -> list[CodeBlock]:
|
|
97
|
+
blocks: list[CodeBlock] = []
|
|
98
|
+
for f in files:
|
|
99
|
+
lang = detect_language(f.path)
|
|
100
|
+
if not f.error and lang == Language.UNKNOWN:
|
|
101
|
+
continue
|
|
102
|
+
try:
|
|
103
|
+
tree = f.tree
|
|
104
|
+
module_scope = symtable.symtable(f.content, f.path, "exec")
|
|
105
|
+
except ParserError, SyntaxError, ValueError:
|
|
106
|
+
self.diagnostics.append(
|
|
107
|
+
PlanDiagnostic(
|
|
108
|
+
code="parser_failed",
|
|
109
|
+
file=f.path,
|
|
110
|
+
severity="error",
|
|
111
|
+
message="Python parsing failed; duplicate analysis is incomplete.",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
continue
|
|
115
|
+
top = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
116
|
+
for node in ast.walk(tree):
|
|
117
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node not in top:
|
|
118
|
+
self.diagnostics.append(
|
|
119
|
+
PlanDiagnostic(
|
|
120
|
+
code="unsupported_scope",
|
|
121
|
+
file=f.path,
|
|
122
|
+
message=f"Skipped {node.name}: methods and nested functions are not standalone utilities.",
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
for node in top:
|
|
126
|
+
content = ast.get_source_segment(f.content, node) or ""
|
|
127
|
+
bindings = {
|
|
128
|
+
s.get_name()
|
|
129
|
+
for s in module_scope.get_symbols()
|
|
130
|
+
if s.is_assigned() or s.is_imported()
|
|
131
|
+
}
|
|
132
|
+
dependencies = _dependencies(content, node.name, bindings)
|
|
133
|
+
if node.decorator_list or dependencies:
|
|
134
|
+
self.diagnostics.append(
|
|
135
|
+
PlanDiagnostic(
|
|
136
|
+
code="dependencies",
|
|
137
|
+
file=f.path,
|
|
138
|
+
message=f"Skipped {node.name}: decorators or external dependencies require review.",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
continue
|
|
142
|
+
try:
|
|
143
|
+
metrics = get_complexity(content)
|
|
144
|
+
except SyntaxError, ValueError:
|
|
145
|
+
continue # invalid snippets have no trustworthy numeric score
|
|
146
|
+
blocks.append(
|
|
147
|
+
CodeBlock(
|
|
148
|
+
id=f"{f.path}:{node.lineno}:{node.name}",
|
|
149
|
+
file=f.path,
|
|
150
|
+
start_line=node.lineno,
|
|
151
|
+
end_line=node.end_lineno or node.lineno,
|
|
152
|
+
content=content,
|
|
153
|
+
language=lang,
|
|
154
|
+
symbol_type="function",
|
|
155
|
+
symbol_name=node.name,
|
|
156
|
+
metrics=metrics,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
return blocks
|
|
160
|
+
|
|
161
|
+
def _create_dedup_change(self, group: list[CodeBlock]) -> FileChange | None:
|
|
162
|
+
primary = group[0]
|
|
163
|
+
return FileChange(
|
|
164
|
+
path=advisory_path(
|
|
165
|
+
"utils", primary.file, f"{primary.symbol_name}_{primary.start_line}_dedup"
|
|
166
|
+
),
|
|
167
|
+
original="",
|
|
168
|
+
modified=primary.content,
|
|
169
|
+
description=(
|
|
170
|
+
f"Proposed shared util for '{primary.symbol_name}' from {len(group)} sites "
|
|
171
|
+
"(suggestion only; applying writes the utility module; "
|
|
172
|
+
"originals and call sites are not rewritten)"
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _dependencies(content: str, name: str, module_bindings: set[str]) -> set[str]:
|
|
178
|
+
"""Conservatively reject names that need the original module's namespace."""
|
|
179
|
+
table = symtable.symtable(content, "<advisory>", "exec")
|
|
180
|
+
required: set[str] = set()
|
|
181
|
+
pending = [table]
|
|
182
|
+
while pending:
|
|
183
|
+
scope = pending.pop()
|
|
184
|
+
required.update(
|
|
185
|
+
s.get_name()
|
|
186
|
+
for s in scope.get_symbols()
|
|
187
|
+
if (s.is_referenced() or s.is_declared_global()) and s.is_global()
|
|
188
|
+
)
|
|
189
|
+
pending.extend(scope.get_children())
|
|
190
|
+
# Recursion is self-contained; builtins need no accompanying import.
|
|
191
|
+
dynamic = {"globals", "locals", "eval", "exec", "__import__"}
|
|
192
|
+
allowed_builtins = set(dir(builtins)) - module_bindings - dynamic
|
|
193
|
+
return required - allowed_builtins - {name}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Idiomatizer agent for transforming code to idiomatic patterns (Python heuristics).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from reducio.agents.base import BaseAgent, ModelRewriteError
|
|
6
|
+
from reducio.models import (
|
|
7
|
+
FileChange,
|
|
8
|
+
IdiomatizeRequest,
|
|
9
|
+
Language,
|
|
10
|
+
PlanDiagnostic,
|
|
11
|
+
PlanningProvenance,
|
|
12
|
+
RefactorPlan,
|
|
13
|
+
)
|
|
14
|
+
from reducio.repo import detect_language
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IdiomatizerAgent(BaseAgent):
|
|
18
|
+
async def idiomatize(self, request: IdiomatizeRequest) -> RefactorPlan:
|
|
19
|
+
self._begin_plan(request.allow_fallback)
|
|
20
|
+
changes = []
|
|
21
|
+
idioms = 0
|
|
22
|
+
for file in request.files:
|
|
23
|
+
try:
|
|
24
|
+
change, count = await self._idiomatize_file(file)
|
|
25
|
+
except ModelRewriteError:
|
|
26
|
+
break
|
|
27
|
+
if change:
|
|
28
|
+
changes.append(change)
|
|
29
|
+
idioms += count
|
|
30
|
+
return self._finalize_plan(
|
|
31
|
+
changes,
|
|
32
|
+
f"Found {idioms} opportunities for idiomatic improvements in {len(changes)} file(s).",
|
|
33
|
+
"idiomatize",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def _idiomatize_file(self, file) -> tuple[FileChange | None, int]:
|
|
37
|
+
content, path = self._file_content_path(file)
|
|
38
|
+
if not file.error and detect_language(path) != Language.PYTHON:
|
|
39
|
+
return None, 0
|
|
40
|
+
try:
|
|
41
|
+
file.tree # Validated once, shared with subsequent analysis.
|
|
42
|
+
except SyntaxError, ValueError:
|
|
43
|
+
self.diagnostics.append(
|
|
44
|
+
PlanDiagnostic(
|
|
45
|
+
code="invalid_source",
|
|
46
|
+
file=path,
|
|
47
|
+
severity="error",
|
|
48
|
+
message="Cannot read or parse source; planning incomplete",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
return None, 0
|
|
52
|
+
if self._llm_enabled():
|
|
53
|
+
try:
|
|
54
|
+
change = await self._llm_rewrite(
|
|
55
|
+
content,
|
|
56
|
+
path,
|
|
57
|
+
"Rewrite the following Python module to be more idiomatic and concise without changing behaviour.",
|
|
58
|
+
"LLM idiomatic rewrite",
|
|
59
|
+
)
|
|
60
|
+
if change:
|
|
61
|
+
change.operation, change.encoding = "replace", file.encoding
|
|
62
|
+
return change, int(change is not None)
|
|
63
|
+
except ModelRewriteError:
|
|
64
|
+
if not self.allow_fallback:
|
|
65
|
+
raise
|
|
66
|
+
change, count = self._idiomatize_python(content, path)
|
|
67
|
+
if change:
|
|
68
|
+
change.operation, change.encoding = "replace", file.encoding
|
|
69
|
+
self.provenance.append(
|
|
70
|
+
PlanningProvenance(
|
|
71
|
+
file=path, engine="heuristic", outcome="proposed" if change else "unchanged"
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
return change, count
|
|
75
|
+
|
|
76
|
+
def _idiomatize_python(self, content: str, path: str) -> tuple[FileChange | None, int]:
|
|
77
|
+
from reducio.idioms import rewrite
|
|
78
|
+
|
|
79
|
+
updated, descriptions, diagnostics = rewrite(content, path)
|
|
80
|
+
self.diagnostics.extend(diagnostics)
|
|
81
|
+
if updated == content:
|
|
82
|
+
return None, 0
|
|
83
|
+
return FileChange(
|
|
84
|
+
path=path, original=content, modified=updated, description="; ".join(descriptions)
|
|
85
|
+
), len(descriptions)
|