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.
Files changed (59) hide show
  1. janito/__init__.py +5 -0
  2. janito/__main__.py +143 -120
  3. janito/callbacks.py +130 -0
  4. janito/cli.py +202 -0
  5. janito/config.py +63 -100
  6. janito/data/instructions.txt +6 -0
  7. janito/test_file.py +4 -0
  8. janito/token_report.py +73 -0
  9. janito/tools/__init__.py +10 -0
  10. janito/tools/decorators.py +84 -0
  11. janito/tools/delete_file.py +44 -0
  12. janito/tools/find_files.py +154 -0
  13. janito/tools/search_text.py +197 -0
  14. janito/tools/str_replace_editor/__init__.py +6 -0
  15. janito/tools/str_replace_editor/editor.py +43 -0
  16. janito/tools/str_replace_editor/handlers.py +338 -0
  17. janito/tools/str_replace_editor/utils.py +88 -0
  18. {janito-0.8.0.dist-info/licenses → janito-0.9.0.dist-info}/LICENSE +2 -2
  19. janito-0.9.0.dist-info/METADATA +9 -0
  20. janito-0.9.0.dist-info/RECORD +23 -0
  21. {janito-0.8.0.dist-info → janito-0.9.0.dist-info}/WHEEL +2 -1
  22. janito-0.9.0.dist-info/entry_points.txt +2 -0
  23. janito-0.9.0.dist-info/top_level.txt +1 -0
  24. janito/agents/__init__.py +0 -22
  25. janito/agents/agent.py +0 -25
  26. janito/agents/claudeai.py +0 -41
  27. janito/agents/deepseekai.py +0 -47
  28. janito/change/applied_blocks.py +0 -34
  29. janito/change/applier.py +0 -167
  30. janito/change/edit_blocks.py +0 -148
  31. janito/change/finder.py +0 -72
  32. janito/change/request.py +0 -144
  33. janito/change/validator.py +0 -87
  34. janito/change/view/content.py +0 -63
  35. janito/change/view/diff.py +0 -44
  36. janito/change/view/panels.py +0 -201
  37. janito/change/view/sections.py +0 -69
  38. janito/change/view/styling.py +0 -140
  39. janito/change/view/summary.py +0 -37
  40. janito/change/view/themes.py +0 -62
  41. janito/change/view/viewer.py +0 -59
  42. janito/cli/__init__.py +0 -2
  43. janito/cli/commands.py +0 -68
  44. janito/cli/functions.py +0 -66
  45. janito/common.py +0 -133
  46. janito/data/change_prompt.txt +0 -81
  47. janito/data/system_prompt.txt +0 -3
  48. janito/qa.py +0 -56
  49. janito/version.py +0 -23
  50. janito/workspace/__init__.py +0 -8
  51. janito/workspace/analysis.py +0 -121
  52. janito/workspace/models.py +0 -97
  53. janito/workspace/show.py +0 -115
  54. janito/workspace/stats.py +0 -42
  55. janito/workspace/workset.py +0 -135
  56. janito/workspace/workspace.py +0 -335
  57. janito-0.8.0.dist-info/METADATA +0 -106
  58. janito-0.8.0.dist-info/RECORD +0 -40
  59. janito-0.8.0.dist-info/entry_points.txt +0 -2
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.2
2
+ Name: janito
3
+ Version: 0.9.0
4
+ Summary: Janito CLI tool
5
+ Requires-Python: >=3.8
6
+ License-File: LICENSE
7
+ Requires-Dist: typer>=0.9.0
8
+ Requires-Dist: rich>=13.0.0
9
+ Requires-Dist: claudine>=0.1.0
@@ -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,,
@@ -1,4 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ janito = janito.__main__:app
@@ -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
@@ -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 ""
@@ -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
@@ -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
- )