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.
Files changed (55) hide show
  1. ai_code_assistant/__init__.py +14 -0
  2. ai_code_assistant/agent/__init__.py +63 -0
  3. ai_code_assistant/agent/code_agent.py +461 -0
  4. ai_code_assistant/agent/code_generator.py +388 -0
  5. ai_code_assistant/agent/code_reviewer.py +365 -0
  6. ai_code_assistant/agent/diff_engine.py +308 -0
  7. ai_code_assistant/agent/file_manager.py +300 -0
  8. ai_code_assistant/agent/intent_classifier.py +284 -0
  9. ai_code_assistant/chat/__init__.py +11 -0
  10. ai_code_assistant/chat/agent_session.py +156 -0
  11. ai_code_assistant/chat/session.py +165 -0
  12. ai_code_assistant/cli.py +1571 -0
  13. ai_code_assistant/config.py +149 -0
  14. ai_code_assistant/editor/__init__.py +8 -0
  15. ai_code_assistant/editor/diff_handler.py +270 -0
  16. ai_code_assistant/editor/file_editor.py +350 -0
  17. ai_code_assistant/editor/prompts.py +146 -0
  18. ai_code_assistant/generator/__init__.py +7 -0
  19. ai_code_assistant/generator/code_gen.py +265 -0
  20. ai_code_assistant/generator/prompts.py +114 -0
  21. ai_code_assistant/git/__init__.py +6 -0
  22. ai_code_assistant/git/commit_generator.py +130 -0
  23. ai_code_assistant/git/manager.py +203 -0
  24. ai_code_assistant/llm.py +111 -0
  25. ai_code_assistant/providers/__init__.py +23 -0
  26. ai_code_assistant/providers/base.py +124 -0
  27. ai_code_assistant/providers/cerebras.py +97 -0
  28. ai_code_assistant/providers/factory.py +148 -0
  29. ai_code_assistant/providers/google.py +103 -0
  30. ai_code_assistant/providers/groq.py +111 -0
  31. ai_code_assistant/providers/ollama.py +86 -0
  32. ai_code_assistant/providers/openai.py +114 -0
  33. ai_code_assistant/providers/openrouter.py +130 -0
  34. ai_code_assistant/py.typed +0 -0
  35. ai_code_assistant/refactor/__init__.py +20 -0
  36. ai_code_assistant/refactor/analyzer.py +189 -0
  37. ai_code_assistant/refactor/change_plan.py +172 -0
  38. ai_code_assistant/refactor/multi_file_editor.py +346 -0
  39. ai_code_assistant/refactor/prompts.py +175 -0
  40. ai_code_assistant/retrieval/__init__.py +19 -0
  41. ai_code_assistant/retrieval/chunker.py +215 -0
  42. ai_code_assistant/retrieval/indexer.py +236 -0
  43. ai_code_assistant/retrieval/search.py +239 -0
  44. ai_code_assistant/reviewer/__init__.py +7 -0
  45. ai_code_assistant/reviewer/analyzer.py +278 -0
  46. ai_code_assistant/reviewer/prompts.py +113 -0
  47. ai_code_assistant/utils/__init__.py +18 -0
  48. ai_code_assistant/utils/file_handler.py +155 -0
  49. ai_code_assistant/utils/formatters.py +259 -0
  50. cognify_code-0.2.0.dist-info/METADATA +383 -0
  51. cognify_code-0.2.0.dist-info/RECORD +55 -0
  52. cognify_code-0.2.0.dist-info/WHEEL +5 -0
  53. cognify_code-0.2.0.dist-info/entry_points.txt +3 -0
  54. cognify_code-0.2.0.dist-info/licenses/LICENSE +22 -0
  55. 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
+