janito 0.8.0__py3-none-any.whl → 0.9.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.
- janito/__init__.py +5 -0
- janito/__main__.py +143 -120
- janito/callbacks.py +130 -0
- janito/cli.py +202 -0
- janito/config.py +63 -100
- janito/data/instructions.txt +6 -0
- janito/test_file.py +4 -0
- janito/token_report.py +73 -0
- janito/tools/__init__.py +10 -0
- janito/tools/decorators.py +84 -0
- janito/tools/delete_file.py +44 -0
- janito/tools/find_files.py +154 -0
- janito/tools/search_text.py +197 -0
- janito/tools/str_replace_editor/__init__.py +6 -0
- janito/tools/str_replace_editor/editor.py +43 -0
- janito/tools/str_replace_editor/handlers.py +338 -0
- janito/tools/str_replace_editor/utils.py +88 -0
- {janito-0.8.0.dist-info/licenses → janito-0.9.0.dist-info}/LICENSE +2 -2
- janito-0.9.0.dist-info/METADATA +9 -0
- janito-0.9.0.dist-info/RECORD +23 -0
- {janito-0.8.0.dist-info → janito-0.9.0.dist-info}/WHEEL +2 -1
- janito-0.9.0.dist-info/entry_points.txt +2 -0
- janito-0.9.0.dist-info/top_level.txt +1 -0
- janito/agents/__init__.py +0 -22
- janito/agents/agent.py +0 -25
- janito/agents/claudeai.py +0 -41
- janito/agents/deepseekai.py +0 -47
- janito/change/applied_blocks.py +0 -34
- janito/change/applier.py +0 -167
- janito/change/edit_blocks.py +0 -148
- janito/change/finder.py +0 -72
- janito/change/request.py +0 -144
- janito/change/validator.py +0 -87
- janito/change/view/content.py +0 -63
- janito/change/view/diff.py +0 -44
- janito/change/view/panels.py +0 -201
- janito/change/view/sections.py +0 -69
- janito/change/view/styling.py +0 -140
- janito/change/view/summary.py +0 -37
- janito/change/view/themes.py +0 -62
- janito/change/view/viewer.py +0 -59
- janito/cli/__init__.py +0 -2
- janito/cli/commands.py +0 -68
- janito/cli/functions.py +0 -66
- janito/common.py +0 -133
- janito/data/change_prompt.txt +0 -81
- janito/data/system_prompt.txt +0 -3
- janito/qa.py +0 -56
- janito/version.py +0 -23
- janito/workspace/__init__.py +0 -8
- janito/workspace/analysis.py +0 -121
- janito/workspace/models.py +0 -97
- janito/workspace/show.py +0 -115
- janito/workspace/stats.py +0 -42
- janito/workspace/workset.py +0 -135
- janito/workspace/workspace.py +0 -335
- janito-0.8.0.dist-info/METADATA +0 -106
- janito-0.8.0.dist-info/RECORD +0 -40
- janito-0.8.0.dist-info/entry_points.txt +0 -2
@@ -0,0 +1,23 @@
|
|
1
|
+
janito/__init__.py,sha256=-e15UAOQqb5aSP4nRNSSpPOQy8y_9nRDbxmNmCSNC_k,52
|
2
|
+
janito/__main__.py,sha256=gskP0c2f1Zu9VwZ9V5QjdGf20wginiuGWa33qeZVDyY,6867
|
3
|
+
janito/callbacks.py,sha256=_kmoRR2lDPQzNLfWPPibilbna4W-abj6hMO1VFICmwY,5288
|
4
|
+
janito/cli.py,sha256=Vbg8W79d-LEB2b4cc58a2HE-8jmZrTvaNoEg2J2543k,9381
|
5
|
+
janito/config.py,sha256=SYaMg3sqWroTaByfxGASleMLxux3s6b-fYZnT-ggqFw,2097
|
6
|
+
janito/test_file.py,sha256=c6GWGdTYG3z-Y5XBao9Tmhmq3G-v0L37OfwLgBo8zIU,126
|
7
|
+
janito/token_report.py,sha256=qLCAPce90Pgh_Q5qssA7irRB1C9e9pOfBC01Wi-ZUug,4006
|
8
|
+
janito/data/instructions.txt,sha256=WkPubK1wPnLG2PpsPikEf7lWQrRW8t1C5p65PV-1qC8,311
|
9
|
+
janito/tools/__init__.py,sha256=izLbyETR5piuFjQZ6ZY6zRgS7Tlx0yXk_wzhPn_CVYc,279
|
10
|
+
janito/tools/decorators.py,sha256=VzUHsoxtxmsd5ic1KAW42eCOT56gjjSzWbEZTcv0RZs,2617
|
11
|
+
janito/tools/delete_file.py,sha256=5JgSFtiF8bpfo0Z15ifj_RFHEHkl9cto1BES9TxIBIA,1245
|
12
|
+
janito/tools/find_files.py,sha256=bN97u3VbFBA78ssXCaEo_cFloni5PE1UW6cSDP9kvjw,5993
|
13
|
+
janito/tools/search_text.py,sha256=nABJJM_vEnMpVPfuLd_tIlVwCfXHTfo1e-K31a8IyJE,7674
|
14
|
+
janito/tools/str_replace_editor/__init__.py,sha256=kYmscmQgft3Jzt3oCNz7k2FiRbJvku6OFDDC3Q_zoAA,144
|
15
|
+
janito/tools/str_replace_editor/editor.py,sha256=XGrBADTlKwlcXat38T5th_KOPrspb9CBCP0g9KRuqmg,1345
|
16
|
+
janito/tools/str_replace_editor/handlers.py,sha256=-7HJinfiJP2s-XHHVAS6TtrNwoNtTH8IJHxuLlYH2pA,12423
|
17
|
+
janito/tools/str_replace_editor/utils.py,sha256=WOkos4bZ5Pe9U_UES6bS_QNISob0GvGN8TQVaRi6RbM,2670
|
18
|
+
janito-0.9.0.dist-info/LICENSE,sha256=6-H8LXExbBIAuT4cyiE-Qy8Bad1K4pagQRVTWr6wkhk,1096
|
19
|
+
janito-0.9.0.dist-info/METADATA,sha256=t8R4TKZwyPRMaS2BbJMhHPwIenKxaPX-Ro7QN7988Rc,216
|
20
|
+
janito-0.9.0.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
21
|
+
janito-0.9.0.dist-info/entry_points.txt,sha256=JMbF_1jg-xQddidpAYkzjOKdw70fy_ymJfcmerY2wIY,47
|
22
|
+
janito-0.9.0.dist-info/top_level.txt,sha256=m0NaVCq0-ivxbazE2-ND0EA9Hmuijj_OGkmCbnBcCig,7
|
23
|
+
janito-0.9.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
janito
|
janito/agents/__init__.py
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
|
3
|
-
# Try to determine backend from available API keys if not explicitly set
|
4
|
-
ai_backend = os.getenv('AI_BACKEND', '').lower()
|
5
|
-
|
6
|
-
if not ai_backend:
|
7
|
-
if os.getenv('ANTHROPIC_API_KEY'):
|
8
|
-
ai_backend = 'claudeai'
|
9
|
-
elif os.getenv('DEEPSEEK_API_KEY'):
|
10
|
-
ai_backend = 'deepseekai'
|
11
|
-
else:
|
12
|
-
raise ValueError("No AI backend API keys found. Please set either ANTHROPIC_API_KEY or DEEPSEEK_API_KEY")
|
13
|
-
|
14
|
-
if ai_backend == "deepseekai":
|
15
|
-
from .deepseekai import DeepSeekAIAgent as AIAgent
|
16
|
-
elif ai_backend == 'claudeai':
|
17
|
-
from .claudeai import ClaudeAIAgent as AIAgent
|
18
|
-
else:
|
19
|
-
raise ValueError(f"Unsupported AI_BACKEND: {ai_backend}")
|
20
|
-
|
21
|
-
# Create a singleton instance
|
22
|
-
agent = AIAgent()
|
janito/agents/agent.py
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
from abc import ABC, abstractmethod
|
2
|
-
from typing import Optional
|
3
|
-
|
4
|
-
class Agent(ABC):
|
5
|
-
"""Abstract base class for AI agents"""
|
6
|
-
friendly_name = "Unknown"
|
7
|
-
|
8
|
-
def __init__(self, api_key: Optional[str] = None):
|
9
|
-
self.api_key = api_key
|
10
|
-
self.last_prompt = None
|
11
|
-
self.last_full_message = None
|
12
|
-
self.last_response = None
|
13
|
-
|
14
|
-
@abstractmethod
|
15
|
-
def send_message(self, message: str, system: str) -> str:
|
16
|
-
"""Send message to the AI agent
|
17
|
-
|
18
|
-
Args:
|
19
|
-
message: The message to send
|
20
|
-
stop_event: Optional event to signal cancellation
|
21
|
-
|
22
|
-
Returns:
|
23
|
-
The response from the AI agent
|
24
|
-
"""
|
25
|
-
pass
|
janito/agents/claudeai.py
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
import anthropic
|
2
|
-
import os
|
3
|
-
|
4
|
-
from .agent import Agent
|
5
|
-
|
6
|
-
class ClaudeAIAgent(Agent):
|
7
|
-
"""Handles interaction with Claude API, including message handling"""
|
8
|
-
DEFAULT_MODEL = "claude-3-5-sonnet-20241022"
|
9
|
-
friendly_name = "Claude"
|
10
|
-
|
11
|
-
def __init__(self):
|
12
|
-
self.api_key = os.getenv('ANTHROPIC_API_KEY')
|
13
|
-
super().__init__(self.api_key)
|
14
|
-
|
15
|
-
if not self.api_key:
|
16
|
-
raise ValueError("ANTHROPIC_API_KEY environment variable is required")
|
17
|
-
self.client = anthropic.Client(api_key=self.api_key)
|
18
|
-
self.model = os.getenv('CLAUDE_MODEL', self.DEFAULT_MODEL)
|
19
|
-
self.last_prompt = None
|
20
|
-
self.last_full_message = None
|
21
|
-
self.last_response = None
|
22
|
-
|
23
|
-
|
24
|
-
def send_message(self, system_message: str, message: str) -> str:
|
25
|
-
"""Send message to Claude API and return response"""
|
26
|
-
# Store the full message
|
27
|
-
self.last_full_message = message
|
28
|
-
|
29
|
-
response = self.client.messages.create(
|
30
|
-
model=self.model, # Use discovered model
|
31
|
-
system=system_message or self.system_message,
|
32
|
-
max_tokens=8192,
|
33
|
-
messages=[
|
34
|
-
{"role": "user", "content": message}
|
35
|
-
],
|
36
|
-
temperature=0,
|
37
|
-
)
|
38
|
-
|
39
|
-
|
40
|
-
# Always return the response, let caller handle cancellation
|
41
|
-
return response
|
janito/agents/deepseekai.py
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
from openai import OpenAI
|
2
|
-
import os
|
3
|
-
from typing import Optional
|
4
|
-
from threading import Event
|
5
|
-
from .agent import Agent
|
6
|
-
|
7
|
-
class DeepSeekAIAgent(Agent):
|
8
|
-
""" DeepSeek AI Agent """
|
9
|
-
DEFAULT_MODEL = "deepseek-chat"
|
10
|
-
friendly_name = "DeepSeek"
|
11
|
-
api_key = None
|
12
|
-
|
13
|
-
def __init__(self, system_prompt: str = None):
|
14
|
-
self.api_key = os.getenv('DEEPSEEK_API_KEY')
|
15
|
-
super().__init__(self.api_key, system_prompt)
|
16
|
-
if not system_prompt:
|
17
|
-
raise ValueError("system_prompt is required")
|
18
|
-
if not self.api_key:
|
19
|
-
raise ValueError("DEEPSEEK_API_KEY environment variable is required")
|
20
|
-
self.client = OpenAI(api_key=self.api_key, base_url="https://api.deepseek.com")
|
21
|
-
self.model = self.DEFAULT_MODEL
|
22
|
-
self.system_message = system_prompt
|
23
|
-
|
24
|
-
def send_message(self, message: str, system_message: str = None) -> str:
|
25
|
-
"""Send message to OpenAI API and return response"""
|
26
|
-
self.last_full_message = message
|
27
|
-
|
28
|
-
try:
|
29
|
-
messages = [
|
30
|
-
{ "role": "system", "content": system_message},
|
31
|
-
{ "role": "user", "content": message}
|
32
|
-
]
|
33
|
-
|
34
|
-
response = self.client.chat.completions.create(
|
35
|
-
model=self.model,
|
36
|
-
messages=messages,
|
37
|
-
max_completion_tokens=4096,
|
38
|
-
temperature=0,
|
39
|
-
)
|
40
|
-
|
41
|
-
response_text = response.choices[0].message.content
|
42
|
-
self.last_response = response_text
|
43
|
-
|
44
|
-
return response
|
45
|
-
|
46
|
-
except KeyboardInterrupt:
|
47
|
-
return ""
|
janito/change/applied_blocks.py
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
from dataclasses import dataclass
|
2
|
-
from pathlib import Path
|
3
|
-
from typing import List, Optional
|
4
|
-
from .edit_blocks import EditType
|
5
|
-
|
6
|
-
@dataclass
|
7
|
-
class AppliedBlock:
|
8
|
-
filename: Path
|
9
|
-
edit_type: EditType
|
10
|
-
reason: str
|
11
|
-
original_content: List[str]
|
12
|
-
modified_content: List[str]
|
13
|
-
range_start: int
|
14
|
-
range_end: int
|
15
|
-
block_marker: Optional[str] = None
|
16
|
-
error_message: Optional[str] = None
|
17
|
-
has_error: bool = False
|
18
|
-
|
19
|
-
@dataclass
|
20
|
-
class AppliedBlocks:
|
21
|
-
blocks: List[AppliedBlock]
|
22
|
-
|
23
|
-
def get_changes_summary(self):
|
24
|
-
"""Get summary info for all applied blocks"""
|
25
|
-
return [{
|
26
|
-
'file': block.filename,
|
27
|
-
'type': block.edit_type.name,
|
28
|
-
'reason': block.reason,
|
29
|
-
'lines_original': len(block.original_content),
|
30
|
-
'lines_modified': len(block.modified_content),
|
31
|
-
'range_start': block.range_start,
|
32
|
-
'range_end': block.range_end,
|
33
|
-
'block_marker': block.block_marker
|
34
|
-
} for block in self.blocks]
|
janito/change/applier.py
DELETED
@@ -1,167 +0,0 @@
|
|
1
|
-
from typing import List
|
2
|
-
from pathlib import Path
|
3
|
-
from janito.config import config
|
4
|
-
from .finder import find_range, EditContentNotFoundError
|
5
|
-
from .edit_blocks import EditType, CodeChange
|
6
|
-
from .applied_blocks import AppliedBlock, AppliedBlocks
|
7
|
-
|
8
|
-
class ChangeApplier:
|
9
|
-
def __init__(self, target_dir: Path):
|
10
|
-
self.target_dir = target_dir
|
11
|
-
self.edits: List[CodeChange] = []
|
12
|
-
self._last_changed_line = 0
|
13
|
-
self.current_file = None
|
14
|
-
self.current_content: List[str] = []
|
15
|
-
self.applied_blocks = AppliedBlocks(blocks=[])
|
16
|
-
|
17
|
-
def add_edit(self, edit: CodeChange):
|
18
|
-
self.edits.append(edit)
|
19
|
-
|
20
|
-
def start_file_edit(self, filename: str, edit_type: EditType):
|
21
|
-
if self.current_file:
|
22
|
-
self.end_file_edit()
|
23
|
-
self._last_changed_line = 0
|
24
|
-
self.current_file = filename
|
25
|
-
self.current_edit_type = edit_type # Store edit type for end_file_edit
|
26
|
-
|
27
|
-
if edit_type == EditType.CREATE:
|
28
|
-
self.current_content = []
|
29
|
-
elif edit_type == EditType.DELETE:
|
30
|
-
if not (self.target_dir / filename).exists():
|
31
|
-
raise FileNotFoundError(f"Cannot delete non-existent file: {filename}")
|
32
|
-
self.current_content = []
|
33
|
-
else:
|
34
|
-
self.current_content = (self.target_dir / filename).read_text(encoding="utf-8").splitlines()
|
35
|
-
|
36
|
-
def end_file_edit(self):
|
37
|
-
if self.current_file:
|
38
|
-
target_path = self.target_dir / self.current_file
|
39
|
-
if hasattr(self, 'current_edit_type') and self.current_edit_type == EditType.DELETE:
|
40
|
-
if target_path.exists():
|
41
|
-
target_path.unlink()
|
42
|
-
else:
|
43
|
-
# Create parent directories if they don't exist
|
44
|
-
target_path.parent.mkdir(parents=True, exist_ok=True)
|
45
|
-
target_path.write_text("\n".join(self.current_content), encoding="utf-8")
|
46
|
-
self.current_file = None
|
47
|
-
|
48
|
-
def apply(self):
|
49
|
-
"""Apply all edits and show summary of changes."""
|
50
|
-
# Ensure target directory exists
|
51
|
-
self.target_dir.mkdir(parents=True, exist_ok=True)
|
52
|
-
|
53
|
-
# Track changes as we apply them
|
54
|
-
changes = []
|
55
|
-
current_file = None
|
56
|
-
|
57
|
-
# Process edits in order as they were added
|
58
|
-
for edit in self.edits:
|
59
|
-
if current_file != edit.filename:
|
60
|
-
self.end_file_edit()
|
61
|
-
self.start_file_edit(str(edit.filename), edit.edit_type)
|
62
|
-
current_file = edit.filename
|
63
|
-
self._apply_and_collect_change(edit)
|
64
|
-
|
65
|
-
self.end_file_edit()
|
66
|
-
|
67
|
-
def _apply_and_collect_change(self, edit: CodeChange) -> AppliedBlock:
|
68
|
-
"""Apply a single edit and collect its change information."""
|
69
|
-
if edit.edit_type == EditType.CREATE:
|
70
|
-
self.current_content = edit.modified
|
71
|
-
applied_block = AppliedBlock(
|
72
|
-
filename=edit.filename,
|
73
|
-
edit_type=edit.edit_type,
|
74
|
-
reason=edit.reason,
|
75
|
-
original_content=[],
|
76
|
-
modified_content=edit.modified,
|
77
|
-
range_start=1,
|
78
|
-
range_end=len(edit.modified),
|
79
|
-
block_marker=edit.block_marker
|
80
|
-
)
|
81
|
-
|
82
|
-
elif edit.edit_type == EditType.DELETE:
|
83
|
-
applied_block = AppliedBlock(
|
84
|
-
filename=edit.filename,
|
85
|
-
edit_type=edit.edit_type,
|
86
|
-
reason=edit.reason,
|
87
|
-
original_content=self.current_content,
|
88
|
-
modified_content=[],
|
89
|
-
range_start=1,
|
90
|
-
range_end=len(self.current_content),
|
91
|
-
block_marker=edit.block_marker
|
92
|
-
)
|
93
|
-
self.current_content = []
|
94
|
-
|
95
|
-
elif edit.edit_type == EditType.CLEAN:
|
96
|
-
try:
|
97
|
-
start_range = find_range(self.current_content, edit.original, self._last_changed_line)
|
98
|
-
try:
|
99
|
-
end_range = find_range(self.current_content, edit.modified, start_range[1])
|
100
|
-
except EditContentNotFoundError:
|
101
|
-
end_range = (start_range[1], start_range[1])
|
102
|
-
|
103
|
-
section = self.current_content[start_range[0]:end_range[1]]
|
104
|
-
applied_block = AppliedBlock(
|
105
|
-
filename=edit.filename,
|
106
|
-
edit_type=edit.edit_type,
|
107
|
-
reason=edit.reason,
|
108
|
-
original_content=section,
|
109
|
-
modified_content=[],
|
110
|
-
range_start=start_range[0] + 1,
|
111
|
-
range_end=end_range[1],
|
112
|
-
block_marker=edit.block_marker
|
113
|
-
)
|
114
|
-
|
115
|
-
self.current_content[start_range[0]:end_range[1]] = []
|
116
|
-
self._last_changed_line = start_range[0]
|
117
|
-
|
118
|
-
except ValueError as e:
|
119
|
-
error_msg = f"Failed to find clean section in {self.current_file}: {e}"
|
120
|
-
applied_block = AppliedBlock(
|
121
|
-
filename=edit.filename,
|
122
|
-
edit_type=edit.edit_type,
|
123
|
-
reason=edit.reason,
|
124
|
-
original_content=self.current_content,
|
125
|
-
modified_content=[],
|
126
|
-
range_start=1,
|
127
|
-
range_end=len(self.current_content),
|
128
|
-
block_marker=edit.block_marker,
|
129
|
-
error_message=error_msg,
|
130
|
-
has_error=True
|
131
|
-
)
|
132
|
-
|
133
|
-
else: # EDIT operation
|
134
|
-
try:
|
135
|
-
edit_range = find_range(self.current_content, edit.original, self._last_changed_line)
|
136
|
-
original_section = self.current_content[edit_range[0]:edit_range[1]]
|
137
|
-
|
138
|
-
applied_block = AppliedBlock(
|
139
|
-
filename=edit.filename,
|
140
|
-
edit_type=edit.edit_type,
|
141
|
-
reason=edit.reason,
|
142
|
-
original_content=original_section,
|
143
|
-
modified_content=edit.modified,
|
144
|
-
range_start=edit_range[0] + 1,
|
145
|
-
range_end=edit_range[0] + len(edit.original),
|
146
|
-
block_marker=edit.block_marker
|
147
|
-
)
|
148
|
-
|
149
|
-
self._last_changed_line = edit_range[0] + len(edit.original)
|
150
|
-
self.current_content[edit_range[0]:edit_range[1]] = edit.modified
|
151
|
-
except EditContentNotFoundError as e:
|
152
|
-
error_msg = f"Failed to find edit section in {self.current_file}: {e}"
|
153
|
-
applied_block = AppliedBlock(
|
154
|
-
filename=edit.filename,
|
155
|
-
edit_type=edit.edit_type,
|
156
|
-
reason=edit.reason,
|
157
|
-
original_content=edit.original,
|
158
|
-
modified_content=edit.modified,
|
159
|
-
range_start=self._last_changed_line + 1,
|
160
|
-
range_end=self._last_changed_line + len(edit.original),
|
161
|
-
block_marker=edit.block_marker,
|
162
|
-
error_message=error_msg,
|
163
|
-
has_error=True
|
164
|
-
)
|
165
|
-
|
166
|
-
self.applied_blocks.blocks.append(applied_block)
|
167
|
-
return applied_block
|
janito/change/edit_blocks.py
DELETED
@@ -1,148 +0,0 @@
|
|
1
|
-
from dataclasses import dataclass
|
2
|
-
from pathlib import Path
|
3
|
-
from enum import Enum, auto
|
4
|
-
from typing import List, Tuple, Dict
|
5
|
-
import string
|
6
|
-
|
7
|
-
class EditType(Enum):
|
8
|
-
CREATE = auto()
|
9
|
-
EDIT = auto()
|
10
|
-
DELETE = auto()
|
11
|
-
CLEAN = auto()
|
12
|
-
|
13
|
-
@dataclass
|
14
|
-
class CodeChange:
|
15
|
-
filename: Path
|
16
|
-
reason: str
|
17
|
-
original: List[str] # Changed from 'before'
|
18
|
-
modified: List[str] # Changed from 'after'
|
19
|
-
edit_type: EditType = EditType.EDIT
|
20
|
-
block_marker: str = None # Track which code block this change came from
|
21
|
-
|
22
|
-
def parse_edit_command(line: str, command: str) -> tuple[Path, str]:
|
23
|
-
"""Parse an Edit or Create command line to extract filename and reason.
|
24
|
-
Expected format: Command filename "reason"
|
25
|
-
Example: Edit path/to/file.py "Add new feature"
|
26
|
-
"""
|
27
|
-
if not line or not line.startswith(command):
|
28
|
-
raise ValueError(f"Invalid command format in line:\n{line}\nExpected: {command} filename \"reason\"")
|
29
|
-
|
30
|
-
# Split by quote to separate filename from reason
|
31
|
-
parts = line.split('"')
|
32
|
-
if len(parts) < 2:
|
33
|
-
raise ValueError(f"Missing reason in quotes in line:\n{line}")
|
34
|
-
|
35
|
-
filename = Path(parts[0].replace(f"{command} ", "").strip())
|
36
|
-
reason = parts[1].strip()
|
37
|
-
|
38
|
-
return filename, reason
|
39
|
-
|
40
|
-
def get_edit_blocks(response: str) -> Tuple[List[CodeChange], str]:
|
41
|
-
"""Parse response text into a list of CodeChange objects and annotated response.
|
42
|
-
|
43
|
-
The format expected from the response follows the system prompt:
|
44
|
-
|
45
|
-
Edit file "reason"
|
46
|
-
<<<< original
|
47
|
-
{original code}
|
48
|
-
>>>> modified
|
49
|
-
{modified code}
|
50
|
-
====
|
51
|
-
|
52
|
-
Clean file "reason"
|
53
|
-
<<<< starting
|
54
|
-
{start marker lines}
|
55
|
-
>>>> ending
|
56
|
-
{end marker lines}
|
57
|
-
====
|
58
|
-
"""
|
59
|
-
edit_blocks = []
|
60
|
-
modified_response = []
|
61
|
-
current_block = []
|
62
|
-
original_content = None
|
63
|
-
current_command = None
|
64
|
-
marker_index = 0
|
65
|
-
in_block = False
|
66
|
-
|
67
|
-
for line in response.splitlines():
|
68
|
-
# Handle command lines
|
69
|
-
if line.startswith(("Edit ", "Create ", "Delete ", "Clean ")):
|
70
|
-
command = line.split(" ")[0]
|
71
|
-
filename, reason = parse_edit_command(line, command)
|
72
|
-
current_command = command
|
73
|
-
# Reset state for new command
|
74
|
-
original_content = None
|
75
|
-
current_block = []
|
76
|
-
# Add marker for this edit block
|
77
|
-
current_marker = string.ascii_uppercase[marker_index]
|
78
|
-
modified_response.append(f"[Edit Block {current_marker}]")
|
79
|
-
marker_index += 1
|
80
|
-
continue
|
81
|
-
|
82
|
-
# Add the line to modified_response unless we're in a code block or it's a block marker
|
83
|
-
if not in_block and not line.startswith(("<<<< ", ">>>> ", "====")):
|
84
|
-
modified_response.append(line)
|
85
|
-
|
86
|
-
# Handle block markers - Update to match system prompt
|
87
|
-
if line.startswith("<<<< original") or line.startswith("<<<< starting"):
|
88
|
-
current_block = []
|
89
|
-
in_block = True
|
90
|
-
if current_command == "Clean":
|
91
|
-
original_content = None
|
92
|
-
elif line.startswith(">>>> modified") or line.startswith(">>>> ending"):
|
93
|
-
if current_command == "Clean":
|
94
|
-
original_content = current_block
|
95
|
-
elif not original_content and current_block:
|
96
|
-
original_content = current_block
|
97
|
-
current_block = []
|
98
|
-
in_block = True
|
99
|
-
elif line == "====": # End of edit block
|
100
|
-
# Trim empty lines at start and end of blocks
|
101
|
-
def trim_block(block: List[str]) -> List[str]:
|
102
|
-
if not block:
|
103
|
-
return []
|
104
|
-
# Remove empty lines at start and end
|
105
|
-
while block and not block[0].strip():
|
106
|
-
block.pop(0)
|
107
|
-
while block and not block[-1].strip():
|
108
|
-
block.pop()
|
109
|
-
return block
|
110
|
-
|
111
|
-
if current_command == "Delete":
|
112
|
-
edit_blocks.append(CodeChange(filename, reason, [], [], EditType.DELETE, current_marker))
|
113
|
-
elif current_command == "Clean":
|
114
|
-
edit_blocks.append(CodeChange(
|
115
|
-
filename, reason,
|
116
|
-
trim_block(original_content or []),
|
117
|
-
trim_block(current_block),
|
118
|
-
EditType.CLEAN,
|
119
|
-
current_marker
|
120
|
-
))
|
121
|
-
elif current_command == "Create":
|
122
|
-
edit_blocks.append(CodeChange(
|
123
|
-
filename, reason,
|
124
|
-
[],
|
125
|
-
trim_block(current_block),
|
126
|
-
EditType.CREATE,
|
127
|
-
current_marker
|
128
|
-
))
|
129
|
-
elif current_command == "Edit":
|
130
|
-
original = trim_block(original_content or [])
|
131
|
-
modified = trim_block(current_block)
|
132
|
-
edit_blocks.append(CodeChange(
|
133
|
-
filename, reason,
|
134
|
-
original,
|
135
|
-
modified,
|
136
|
-
EditType.EDIT,
|
137
|
-
current_marker
|
138
|
-
))
|
139
|
-
|
140
|
-
# Reset state after block is completed
|
141
|
-
current_block = []
|
142
|
-
in_block = False
|
143
|
-
current_command = None
|
144
|
-
original_content = None
|
145
|
-
elif in_block:
|
146
|
-
current_block.append(line)
|
147
|
-
|
148
|
-
return edit_blocks, "\n".join(modified_response)
|
janito/change/finder.py
DELETED
@@ -1,72 +0,0 @@
|
|
1
|
-
from typing import List, Tuple
|
2
|
-
from difflib import SequenceMatcher
|
3
|
-
|
4
|
-
class EditContentNotFoundError(ValueError):
|
5
|
-
"""Raised when edit content cannot be found in the target file."""
|
6
|
-
pass
|
7
|
-
|
8
|
-
SIMILARITY_THRESHOLD = 0.8 # Minimum similarity required for a match
|
9
|
-
|
10
|
-
def find_range(full_lines: List[str], changed_lines: List[str], start: int = 0) -> Tuple[int, int]:
|
11
|
-
"""Find the range of the first occurrence of the changed_lines in the full_lines list.
|
12
|
-
|
13
|
-
Args:
|
14
|
-
full_lines: The complete text content to search within
|
15
|
-
changed_lines: The block of lines to find
|
16
|
-
start: The line number to start searching from
|
17
|
-
|
18
|
-
Returns:
|
19
|
-
Tuple of (start_line, end_line) where the block was found
|
20
|
-
|
21
|
-
Raises:
|
22
|
-
ValueError: If no matching block is found with sufficient similarity
|
23
|
-
"""
|
24
|
-
_validate_inputs(full_lines, changed_lines, start)
|
25
|
-
|
26
|
-
if not changed_lines:
|
27
|
-
return (start, start)
|
28
|
-
|
29
|
-
best_match, best_score = _find_best_matching_block(full_lines, changed_lines, start)
|
30
|
-
|
31
|
-
if not best_match or best_score < SIMILARITY_THRESHOLD:
|
32
|
-
_raise_no_match_error(changed_lines, start, best_score)
|
33
|
-
|
34
|
-
return best_match
|
35
|
-
|
36
|
-
def _validate_inputs(full_lines: List[str], changed_lines: List[str], start: int) -> None:
|
37
|
-
if start >= len(full_lines):
|
38
|
-
raise ValueError(f"Start position {start} is beyond content length {len(full_lines)}")
|
39
|
-
|
40
|
-
def _find_best_matching_block(full_lines: List[str], changed_lines: List[str], start: int) -> Tuple[Tuple[int, int], float]:
|
41
|
-
best_match = None
|
42
|
-
best_score = 0.0
|
43
|
-
|
44
|
-
for i in range(start, len(full_lines) - len(changed_lines) + 1):
|
45
|
-
window = full_lines[i:i + len(changed_lines)]
|
46
|
-
if len(window) != len(changed_lines):
|
47
|
-
continue
|
48
|
-
|
49
|
-
similarity = _calculate_similarity(window, changed_lines)
|
50
|
-
|
51
|
-
if similarity > best_score:
|
52
|
-
best_score = similarity
|
53
|
-
best_match = (i, i + len(changed_lines))
|
54
|
-
|
55
|
-
if similarity == 1.0: # Early exit on perfect match
|
56
|
-
break
|
57
|
-
|
58
|
-
return best_match, best_score
|
59
|
-
|
60
|
-
def _calculate_similarity(window: List[str], changed_lines: List[str]) -> float:
|
61
|
-
return sum(
|
62
|
-
SequenceMatcher(None, a, b).ratio()
|
63
|
-
for a, b in zip(window, changed_lines)
|
64
|
-
) / len(changed_lines)
|
65
|
-
|
66
|
-
def _raise_no_match_error(changed_lines: List[str], start: int, best_score: float) -> None:
|
67
|
-
sample = "\n".join(changed_lines[:3]) + ("..." if len(changed_lines) > 3 else "")
|
68
|
-
raise EditContentNotFoundError(
|
69
|
-
f"Could not find matching block after line {start}. "
|
70
|
-
f"Looking for:\n{sample}\n"
|
71
|
-
f"Best match score: {best_score:.2f}"
|
72
|
-
)
|