cognify-code 0.2.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.
- ai_code_assistant/__init__.py +14 -0
- ai_code_assistant/agent/__init__.py +63 -0
- ai_code_assistant/agent/code_agent.py +461 -0
- ai_code_assistant/agent/code_generator.py +388 -0
- ai_code_assistant/agent/code_reviewer.py +365 -0
- ai_code_assistant/agent/diff_engine.py +308 -0
- ai_code_assistant/agent/file_manager.py +300 -0
- ai_code_assistant/agent/intent_classifier.py +284 -0
- ai_code_assistant/chat/__init__.py +11 -0
- ai_code_assistant/chat/agent_session.py +156 -0
- ai_code_assistant/chat/session.py +165 -0
- ai_code_assistant/cli.py +1571 -0
- ai_code_assistant/config.py +149 -0
- ai_code_assistant/editor/__init__.py +8 -0
- ai_code_assistant/editor/diff_handler.py +270 -0
- ai_code_assistant/editor/file_editor.py +350 -0
- ai_code_assistant/editor/prompts.py +146 -0
- ai_code_assistant/generator/__init__.py +7 -0
- ai_code_assistant/generator/code_gen.py +265 -0
- ai_code_assistant/generator/prompts.py +114 -0
- ai_code_assistant/git/__init__.py +6 -0
- ai_code_assistant/git/commit_generator.py +130 -0
- ai_code_assistant/git/manager.py +203 -0
- ai_code_assistant/llm.py +111 -0
- ai_code_assistant/providers/__init__.py +23 -0
- ai_code_assistant/providers/base.py +124 -0
- ai_code_assistant/providers/cerebras.py +97 -0
- ai_code_assistant/providers/factory.py +148 -0
- ai_code_assistant/providers/google.py +103 -0
- ai_code_assistant/providers/groq.py +111 -0
- ai_code_assistant/providers/ollama.py +86 -0
- ai_code_assistant/providers/openai.py +114 -0
- ai_code_assistant/providers/openrouter.py +130 -0
- ai_code_assistant/py.typed +0 -0
- ai_code_assistant/refactor/__init__.py +20 -0
- ai_code_assistant/refactor/analyzer.py +189 -0
- ai_code_assistant/refactor/change_plan.py +172 -0
- ai_code_assistant/refactor/multi_file_editor.py +346 -0
- ai_code_assistant/refactor/prompts.py +175 -0
- ai_code_assistant/retrieval/__init__.py +19 -0
- ai_code_assistant/retrieval/chunker.py +215 -0
- ai_code_assistant/retrieval/indexer.py +236 -0
- ai_code_assistant/retrieval/search.py +239 -0
- ai_code_assistant/reviewer/__init__.py +7 -0
- ai_code_assistant/reviewer/analyzer.py +278 -0
- ai_code_assistant/reviewer/prompts.py +113 -0
- ai_code_assistant/utils/__init__.py +18 -0
- ai_code_assistant/utils/file_handler.py +155 -0
- ai_code_assistant/utils/formatters.py +259 -0
- cognify_code-0.2.0.dist-info/METADATA +383 -0
- cognify_code-0.2.0.dist-info/RECORD +55 -0
- cognify_code-0.2.0.dist-info/WHEEL +5 -0
- cognify_code-0.2.0.dist-info/entry_points.txt +3 -0
- cognify_code-0.2.0.dist-info/licenses/LICENSE +22 -0
- cognify_code-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Multi-file refactoring module for AI Code Assistant."""
|
|
2
|
+
|
|
3
|
+
from ai_code_assistant.refactor.change_plan import (
|
|
4
|
+
ChangePlan,
|
|
5
|
+
FileChange,
|
|
6
|
+
ChangeType,
|
|
7
|
+
RefactorResult,
|
|
8
|
+
)
|
|
9
|
+
from ai_code_assistant.refactor.multi_file_editor import MultiFileEditor
|
|
10
|
+
from ai_code_assistant.refactor.analyzer import RefactorAnalyzer
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ChangePlan",
|
|
14
|
+
"FileChange",
|
|
15
|
+
"ChangeType",
|
|
16
|
+
"RefactorResult",
|
|
17
|
+
"MultiFileEditor",
|
|
18
|
+
"RefactorAnalyzer",
|
|
19
|
+
]
|
|
20
|
+
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Analyzer for determining refactoring scope and impact."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from ai_code_assistant.config import Config, get_language_by_extension
|
|
9
|
+
from ai_code_assistant.llm import LLMManager
|
|
10
|
+
from ai_code_assistant.refactor.prompts import ANALYZE_REFACTOR_PROMPT
|
|
11
|
+
from ai_code_assistant.refactor.change_plan import ChangePlan, FileChange, ChangeType
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RefactorAnalyzer:
|
|
15
|
+
"""Analyzes codebase to determine refactoring scope."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: Config, llm_manager: LLMManager):
|
|
18
|
+
"""Initialize the refactor analyzer.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
config: Application configuration
|
|
22
|
+
llm_manager: LLM manager for AI interactions
|
|
23
|
+
"""
|
|
24
|
+
self.config = config
|
|
25
|
+
self.llm = llm_manager
|
|
26
|
+
|
|
27
|
+
def analyze(
|
|
28
|
+
self,
|
|
29
|
+
instruction: str,
|
|
30
|
+
files: List[Path],
|
|
31
|
+
max_files: int = 20,
|
|
32
|
+
) -> ChangePlan:
|
|
33
|
+
"""Analyze files to create a refactoring plan.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
instruction: Refactoring instruction
|
|
37
|
+
files: List of files to analyze
|
|
38
|
+
max_files: Maximum number of files to include
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
ChangePlan with proposed changes
|
|
42
|
+
"""
|
|
43
|
+
# Limit files to analyze
|
|
44
|
+
files = files[:max_files]
|
|
45
|
+
|
|
46
|
+
# Read file contents
|
|
47
|
+
file_contents = self._read_files(files)
|
|
48
|
+
|
|
49
|
+
if not file_contents:
|
|
50
|
+
return ChangePlan(
|
|
51
|
+
instruction=instruction,
|
|
52
|
+
summary="No files to analyze",
|
|
53
|
+
changes=[],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Format file contents for prompt
|
|
57
|
+
formatted_contents = self._format_file_contents(file_contents)
|
|
58
|
+
|
|
59
|
+
# Get analysis from LLM
|
|
60
|
+
try:
|
|
61
|
+
response = self.llm.invoke_with_template(
|
|
62
|
+
ANALYZE_REFACTOR_PROMPT,
|
|
63
|
+
instruction=instruction,
|
|
64
|
+
file_contents=formatted_contents,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return self._parse_analysis(instruction, response)
|
|
68
|
+
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return ChangePlan(
|
|
71
|
+
instruction=instruction,
|
|
72
|
+
summary=f"Analysis failed: {str(e)}",
|
|
73
|
+
changes=[],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _read_files(self, files: List[Path]) -> dict:
|
|
77
|
+
"""Read contents of files.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
files: List of file paths
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dict mapping file path to content
|
|
84
|
+
"""
|
|
85
|
+
contents = {}
|
|
86
|
+
max_size = getattr(self.config, 'refactor', None)
|
|
87
|
+
max_size_kb = max_size.max_file_size_kb if max_size else 500
|
|
88
|
+
|
|
89
|
+
for file_path in files:
|
|
90
|
+
if not file_path.exists():
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
# Check file size
|
|
94
|
+
if file_path.stat().st_size > max_size_kb * 1024:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
contents[str(file_path)] = file_path.read_text(encoding="utf-8")
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
return contents
|
|
103
|
+
|
|
104
|
+
def _format_file_contents(self, contents: dict) -> str:
|
|
105
|
+
"""Format file contents for prompt.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
contents: Dict mapping file path to content
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Formatted string with all file contents
|
|
112
|
+
"""
|
|
113
|
+
parts = []
|
|
114
|
+
for file_path, content in contents.items():
|
|
115
|
+
language = self._detect_language(file_path)
|
|
116
|
+
parts.append(f"### {file_path}\n```{language}\n{content}\n```\n")
|
|
117
|
+
return "\n".join(parts)
|
|
118
|
+
|
|
119
|
+
def _detect_language(self, file_path: str) -> str:
|
|
120
|
+
"""Detect language from file path."""
|
|
121
|
+
path = Path(file_path)
|
|
122
|
+
lang = get_language_by_extension(self.config, path)
|
|
123
|
+
return lang or "text"
|
|
124
|
+
|
|
125
|
+
def _parse_analysis(self, instruction: str, response: str) -> ChangePlan:
|
|
126
|
+
"""Parse LLM analysis response into ChangePlan.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
instruction: Original instruction
|
|
130
|
+
response: LLM response
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Parsed ChangePlan
|
|
134
|
+
"""
|
|
135
|
+
# Try to extract JSON from response
|
|
136
|
+
json_data = self._extract_json(response)
|
|
137
|
+
|
|
138
|
+
if not json_data:
|
|
139
|
+
return ChangePlan(
|
|
140
|
+
instruction=instruction,
|
|
141
|
+
summary="Could not parse analysis response",
|
|
142
|
+
changes=[],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Parse changes
|
|
146
|
+
changes = []
|
|
147
|
+
for file_data in json_data.get("affected_files", []):
|
|
148
|
+
change_type_str = file_data.get("change_type", "modify").lower()
|
|
149
|
+
try:
|
|
150
|
+
change_type = ChangeType(change_type_str)
|
|
151
|
+
except ValueError:
|
|
152
|
+
change_type = ChangeType.MODIFY
|
|
153
|
+
|
|
154
|
+
changes.append(FileChange(
|
|
155
|
+
file_path=file_data.get("file_path", ""),
|
|
156
|
+
change_type=change_type,
|
|
157
|
+
description=file_data.get("description", ""),
|
|
158
|
+
priority=file_data.get("priority", "medium"),
|
|
159
|
+
depends_on=file_data.get("depends_on", []),
|
|
160
|
+
))
|
|
161
|
+
|
|
162
|
+
return ChangePlan(
|
|
163
|
+
instruction=instruction,
|
|
164
|
+
summary=json_data.get("summary", ""),
|
|
165
|
+
changes=changes,
|
|
166
|
+
risks=json_data.get("risks", []),
|
|
167
|
+
complexity=json_data.get("estimated_complexity", "medium"),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _extract_json(self, text: str) -> Optional[dict]:
|
|
171
|
+
"""Extract JSON from text response."""
|
|
172
|
+
# Try to find JSON in code block
|
|
173
|
+
match = re.search(r"```(?:json)?\s*\n?(.*?)\n?```", text, re.DOTALL)
|
|
174
|
+
if match:
|
|
175
|
+
try:
|
|
176
|
+
return json.loads(match.group(1))
|
|
177
|
+
except json.JSONDecodeError:
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
# Try to find raw JSON
|
|
181
|
+
match = re.search(r"\{.*\}", text, re.DOTALL)
|
|
182
|
+
if match:
|
|
183
|
+
try:
|
|
184
|
+
return json.loads(match.group())
|
|
185
|
+
except json.JSONDecodeError:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
return None
|
|
189
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Data structures for multi-file refactoring plans."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Literal, Optional
|
|
7
|
+
|
|
8
|
+
from ai_code_assistant.editor.diff_handler import DiffResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChangeType(str, Enum):
|
|
12
|
+
"""Type of file change."""
|
|
13
|
+
MODIFY = "modify"
|
|
14
|
+
CREATE = "create"
|
|
15
|
+
DELETE = "delete"
|
|
16
|
+
RENAME = "rename"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class FileChange:
|
|
21
|
+
"""Represents a planned change to a single file."""
|
|
22
|
+
file_path: str
|
|
23
|
+
change_type: ChangeType
|
|
24
|
+
description: str
|
|
25
|
+
priority: Literal["high", "medium", "low"] = "medium"
|
|
26
|
+
depends_on: List[str] = field(default_factory=list)
|
|
27
|
+
original_content: str = ""
|
|
28
|
+
new_content: str = ""
|
|
29
|
+
new_path: Optional[str] = None # For rename operations
|
|
30
|
+
diff: Optional[DiffResult] = None
|
|
31
|
+
applied: bool = False
|
|
32
|
+
error: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def success(self) -> bool:
|
|
36
|
+
return self.error is None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def has_changes(self) -> bool:
|
|
40
|
+
if self.change_type == ChangeType.DELETE:
|
|
41
|
+
return True
|
|
42
|
+
if self.change_type == ChangeType.CREATE:
|
|
43
|
+
return bool(self.new_content)
|
|
44
|
+
return self.original_content != self.new_content
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict:
|
|
47
|
+
return {
|
|
48
|
+
"file_path": self.file_path,
|
|
49
|
+
"change_type": self.change_type.value,
|
|
50
|
+
"description": self.description,
|
|
51
|
+
"priority": self.priority,
|
|
52
|
+
"depends_on": self.depends_on,
|
|
53
|
+
"has_changes": self.has_changes,
|
|
54
|
+
"applied": self.applied,
|
|
55
|
+
"error": self.error,
|
|
56
|
+
"new_path": self.new_path,
|
|
57
|
+
"diff": self.diff.to_dict() if self.diff else None,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ChangePlan:
|
|
63
|
+
"""A plan for multi-file refactoring."""
|
|
64
|
+
instruction: str
|
|
65
|
+
summary: str
|
|
66
|
+
changes: List[FileChange] = field(default_factory=list)
|
|
67
|
+
risks: List[str] = field(default_factory=list)
|
|
68
|
+
complexity: Literal["low", "medium", "high"] = "medium"
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def total_files(self) -> int:
|
|
72
|
+
return len(self.changes)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def files_to_modify(self) -> List[FileChange]:
|
|
76
|
+
return [c for c in self.changes if c.change_type == ChangeType.MODIFY]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def files_to_create(self) -> List[FileChange]:
|
|
80
|
+
return [c for c in self.changes if c.change_type == ChangeType.CREATE]
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def files_to_delete(self) -> List[FileChange]:
|
|
84
|
+
return [c for c in self.changes if c.change_type == ChangeType.DELETE]
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def files_to_rename(self) -> List[FileChange]:
|
|
88
|
+
return [c for c in self.changes if c.change_type == ChangeType.RENAME]
|
|
89
|
+
|
|
90
|
+
def get_ordered_changes(self) -> List[FileChange]:
|
|
91
|
+
"""Get changes ordered by dependencies and priority."""
|
|
92
|
+
priority_order = {"high": 0, "medium": 1, "low": 2}
|
|
93
|
+
|
|
94
|
+
# Simple topological sort based on dependencies
|
|
95
|
+
ordered = []
|
|
96
|
+
remaining = list(self.changes)
|
|
97
|
+
applied_paths = set()
|
|
98
|
+
|
|
99
|
+
while remaining:
|
|
100
|
+
# Find changes with no unmet dependencies
|
|
101
|
+
ready = [
|
|
102
|
+
c for c in remaining
|
|
103
|
+
if all(dep in applied_paths for dep in c.depends_on)
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
if not ready:
|
|
107
|
+
# Circular dependency or missing dependency - add remaining
|
|
108
|
+
ready = remaining
|
|
109
|
+
|
|
110
|
+
# Sort by priority
|
|
111
|
+
ready.sort(key=lambda c: priority_order.get(c.priority, 1))
|
|
112
|
+
|
|
113
|
+
# Add first ready change
|
|
114
|
+
change = ready[0]
|
|
115
|
+
ordered.append(change)
|
|
116
|
+
applied_paths.add(change.file_path)
|
|
117
|
+
remaining.remove(change)
|
|
118
|
+
|
|
119
|
+
return ordered
|
|
120
|
+
|
|
121
|
+
def to_dict(self) -> dict:
|
|
122
|
+
return {
|
|
123
|
+
"instruction": self.instruction,
|
|
124
|
+
"summary": self.summary,
|
|
125
|
+
"total_files": self.total_files,
|
|
126
|
+
"complexity": self.complexity,
|
|
127
|
+
"risks": self.risks,
|
|
128
|
+
"changes": [c.to_dict() for c in self.changes],
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class RefactorResult:
|
|
134
|
+
"""Result of a multi-file refactoring operation."""
|
|
135
|
+
plan: ChangePlan
|
|
136
|
+
applied: bool = False
|
|
137
|
+
backup_dir: Optional[str] = None
|
|
138
|
+
error: Optional[str] = None
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def success(self) -> bool:
|
|
142
|
+
return self.error is None
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def files_changed(self) -> int:
|
|
146
|
+
return sum(1 for c in self.plan.changes if c.applied)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def files_failed(self) -> int:
|
|
150
|
+
return sum(1 for c in self.plan.changes if c.error)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def total_additions(self) -> int:
|
|
154
|
+
return sum(c.diff.additions for c in self.plan.changes if c.diff)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def total_deletions(self) -> int:
|
|
158
|
+
return sum(c.diff.deletions for c in self.plan.changes if c.diff)
|
|
159
|
+
|
|
160
|
+
def to_dict(self) -> dict:
|
|
161
|
+
return {
|
|
162
|
+
"success": self.success,
|
|
163
|
+
"applied": self.applied,
|
|
164
|
+
"files_changed": self.files_changed,
|
|
165
|
+
"files_failed": self.files_failed,
|
|
166
|
+
"total_additions": self.total_additions,
|
|
167
|
+
"total_deletions": self.total_deletions,
|
|
168
|
+
"backup_dir": self.backup_dir,
|
|
169
|
+
"error": self.error,
|
|
170
|
+
"plan": self.plan.to_dict(),
|
|
171
|
+
}
|
|
172
|
+
|