janito 0.4.0__py3-none-any.whl → 0.6.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 +1 -1
- janito/__main__.py +102 -326
- janito/agents/__init__.py +16 -0
- janito/agents/agent.py +21 -0
- janito/{claude.py → agents/claudeai.py} +13 -17
- janito/agents/openai.py +53 -0
- janito/agents/test.py +34 -0
- janito/change/__init__.py +32 -0
- janito/change/__main__.py +0 -0
- janito/change/analysis/__init__.py +23 -0
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +61 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/change/analysis/prompts.py +98 -0
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +171 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +245 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +131 -0
- janito/change/history.py +44 -0
- janito/change/operations.py +7 -0
- janito/change/parser.py +289 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +126 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +251 -0
- janito/change/viewer/__init__.py +11 -0
- janito/change/viewer/content.py +66 -0
- janito/change/viewer/diff.py +43 -0
- janito/change/viewer/pager.py +56 -0
- janito/change/viewer/panels.py +555 -0
- janito/change/viewer/styling.py +103 -0
- janito/change/viewer/themes.py +55 -0
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/__init__.py +2 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +45 -0
- janito/cli/functions.py +111 -0
- janito/cli/handlers/ask.py +22 -0
- janito/cli/handlers/demo.py +22 -0
- janito/cli/handlers/request.py +24 -0
- janito/cli/handlers/scan.py +9 -0
- janito/cli/history.py +61 -0
- janito/cli/registry.py +26 -0
- janito/common.py +41 -10
- janito/config.py +71 -6
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompts.py +1 -65
- janito/qa.py +8 -5
- janito/review.py +13 -0
- janito/search_replace/README.md +146 -0
- janito/search_replace/__init__.py +6 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +119 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +299 -0
- janito/shell/__init__.py +39 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +195 -0
- janito/shell/handlers.py +122 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +52 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +7 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/manager.py +48 -0
- janito/workspace/scan.py +232 -0
- janito-0.6.0.dist-info/METADATA +185 -0
- janito-0.6.0.dist-info/RECORD +95 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
- janito/analysis.py +0 -281
- janito/changeapplier.py +0 -436
- janito/changeviewer.py +0 -350
- janito/console.py +0 -330
- janito/contentchange.py +0 -84
- janito/contextparser.py +0 -113
- janito/fileparser.py +0 -125
- janito/scan.py +0 -137
- janito-0.4.0.dist-info/METADATA +0 -164
- janito-0.4.0.dist-info/RECORD +0 -21
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
janito/__init__.py
CHANGED
janito/__main__.py
CHANGED
@@ -1,356 +1,132 @@
|
|
1
|
-
import sys
|
2
1
|
import typer
|
3
|
-
from typing import Optional,
|
2
|
+
from typing import Optional, List, Set
|
4
3
|
from pathlib import Path
|
5
|
-
from
|
6
|
-
import
|
7
|
-
from janito.prompts import (
|
8
|
-
build_selected_option_prompt,
|
9
|
-
SYSTEM_PROMPT,
|
10
|
-
)
|
4
|
+
from rich.text import Text
|
5
|
+
from rich import print as rich_print
|
11
6
|
from rich.console import Console
|
12
|
-
from rich.markdown import Markdown
|
13
|
-
import re
|
14
|
-
import tempfile
|
15
|
-
import json
|
16
|
-
from rich.syntax import Syntax
|
17
|
-
from janito.contentchange import (
|
18
|
-
handle_changes_file,
|
19
|
-
get_file_type,
|
20
|
-
parse_block_changes,
|
21
|
-
preview_and_apply_changes,
|
22
|
-
format_parsed_changes,
|
23
|
-
)
|
24
|
-
from rich.table import Table
|
25
|
-
from rich.columns import Columns
|
26
|
-
from rich.panel import Panel
|
27
7
|
from rich.text import Text
|
28
|
-
from
|
29
|
-
from rich import box
|
30
|
-
from datetime import datetime, timezone
|
31
|
-
from itertools import chain
|
32
|
-
from janito.scan import collect_files_content, is_dir_empty, preview_scan
|
33
|
-
from janito.qa import ask_question, display_answer
|
34
|
-
from rich.prompt import Prompt, Confirm
|
35
|
-
from janito.config import config
|
36
|
-
from janito.version import get_version
|
37
|
-
from janito.common import progress_send_message
|
38
|
-
from janito.analysis import format_analysis, build_request_analysis_prompt, parse_analysis_options, get_history_file_type, AnalysisOption
|
39
|
-
|
40
|
-
|
41
|
-
def prompt_user(message: str, choices: List[str] = None) -> str:
|
42
|
-
"""Display a prominent user prompt with optional choices using consistent colors"""
|
43
|
-
console = Console()
|
44
|
-
|
45
|
-
# Define consistent colors
|
46
|
-
COLORS = {
|
47
|
-
'primary': '#729FCF', # Soft blue for primary elements
|
48
|
-
'secondary': '#8AE234', # Bright green for actions/success
|
49
|
-
'accent': '#AD7FA8', # Purple for accents
|
50
|
-
'muted': '#7F9F7F', # Muted green for less important text
|
51
|
-
}
|
52
|
-
|
53
|
-
console.print()
|
54
|
-
console.print(Rule(" User Input Required ", style=f"bold {COLORS['primary']}"))
|
55
|
-
|
56
|
-
if choices:
|
57
|
-
choice_text = f"[{COLORS['accent']}]Options: {', '.join(choices)}[/{COLORS['accent']}]"
|
58
|
-
console.print(Panel(choice_text, box=box.ROUNDED, border_style=COLORS['primary']))
|
59
|
-
|
60
|
-
return Prompt.ask(f"[bold {COLORS['secondary']}]> {message}[/bold {COLORS['secondary']}]")
|
8
|
+
from .version import get_version
|
61
9
|
|
62
|
-
|
63
|
-
|
64
|
-
return letter.upper() in options or letter.upper() == 'M'
|
65
|
-
|
66
|
-
def get_option_selection() -> str:
|
67
|
-
"""Get user input for option selection with modify option"""
|
68
|
-
console = Console()
|
69
|
-
console.print("\n[cyan]Enter option letter or 'M' to modify request[/cyan]")
|
70
|
-
while True:
|
71
|
-
letter = prompt_user("Select option").strip().upper()
|
72
|
-
if letter == 'M' or (letter.isalpha() and len(letter) == 1):
|
73
|
-
return letter
|
74
|
-
console.print("[red]Please enter a valid letter or 'M'[/red]")
|
75
|
-
|
76
|
-
def get_changes_history_path(workdir: Path) -> Path:
|
77
|
-
"""Create and return the changes history directory path"""
|
78
|
-
changes_history_dir = workdir / '.janito' / 'changes_history'
|
79
|
-
changes_history_dir.mkdir(parents=True, exist_ok=True)
|
80
|
-
return changes_history_dir
|
81
|
-
|
82
|
-
def get_timestamp() -> str:
|
83
|
-
"""Get current UTC timestamp in YMD_HMS format with leading zeros"""
|
84
|
-
return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
10
|
+
from janito.agents import agent
|
11
|
+
from janito.config import config
|
85
12
|
|
86
|
-
|
87
|
-
"""Save prompt to a named temporary file that won't be deleted"""
|
88
|
-
temp_file = tempfile.NamedTemporaryFile(prefix='selected_', suffix='.txt', delete=False)
|
89
|
-
temp_path = Path(temp_file.name)
|
90
|
-
temp_path.write_text(prompt)
|
91
|
-
return temp_path
|
13
|
+
from .cli.commands import handle_request, handle_ask, handle_play, handle_scan
|
92
14
|
|
93
|
-
|
94
|
-
"""Save content to a timestamped file in changes history directory"""
|
95
|
-
changes_history_dir = get_changes_history_path(workdir)
|
96
|
-
timestamp = get_timestamp()
|
97
|
-
filename = f"{timestamp}_{prefix}.txt"
|
98
|
-
file_path = changes_history_dir / filename
|
99
|
-
file_path.write_text(content)
|
100
|
-
return file_path
|
15
|
+
app = typer.Typer(add_completion=False)
|
101
16
|
|
102
|
-
def
|
103
|
-
"""
|
104
|
-
console = Console()
|
17
|
+
def validate_paths(paths: Optional[List[Path]]) -> Optional[List[Path]]:
|
18
|
+
"""Validate include paths for duplicates.
|
105
19
|
|
106
|
-
|
107
|
-
|
108
|
-
console.print(Panel(
|
109
|
-
Text(request, style="white"),
|
110
|
-
border_style="blue",
|
111
|
-
title="Previous Request",
|
112
|
-
padding=(1, 2)
|
113
|
-
))
|
114
|
-
|
115
|
-
# Get modified request with clear prompt
|
116
|
-
console.print("\n[bold cyan]Enter modified request below:[/bold cyan]")
|
117
|
-
console.print("[dim](Press Enter to submit, Ctrl+C to cancel)[/dim]")
|
118
|
-
try:
|
119
|
-
new_request = prompt_user("Modified request")
|
120
|
-
if not new_request.strip():
|
121
|
-
console.print("[yellow]No changes made, keeping original request[/yellow]")
|
122
|
-
return request
|
123
|
-
return new_request
|
124
|
-
except KeyboardInterrupt:
|
125
|
-
console.print("\n[yellow]Modification cancelled, keeping original request[/yellow]")
|
126
|
-
return request
|
127
|
-
|
128
|
-
def format_option_text(option: AnalysisOption) -> str:
|
129
|
-
"""Format an AnalysisOption into a string representation"""
|
130
|
-
option_text = f"Option {option.letter}:\n"
|
131
|
-
option_text += f"Summary: {option.summary}\n\n"
|
132
|
-
option_text += "Description:\n"
|
133
|
-
for item in option.description_items:
|
134
|
-
option_text += f"- {item}\n"
|
135
|
-
option_text += "\nAffected files:\n"
|
136
|
-
for file in option.affected_files:
|
137
|
-
option_text += f"- {file}\n"
|
138
|
-
return option_text
|
139
|
-
|
140
|
-
def handle_option_selection(claude: ClaudeAPIAgent, initial_response: str, request: str, raw: bool = False, workdir: Optional[Path] = None, include: Optional[List[Path]] = None) -> None:
|
141
|
-
"""Handle option selection and implementation details"""
|
142
|
-
options = parse_analysis_options(initial_response)
|
143
|
-
if not options:
|
144
|
-
console = Console()
|
145
|
-
console.print("[red]No valid options found in the response[/red]")
|
146
|
-
return
|
147
|
-
|
148
|
-
while True:
|
149
|
-
option = get_option_selection()
|
20
|
+
Args:
|
21
|
+
paths: List of paths to validate, or None if no paths provided
|
150
22
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
format_analysis(initial_response, raw, claude)
|
168
|
-
options = parse_analysis_options(initial_response)
|
169
|
-
if not options:
|
170
|
-
console = Console()
|
171
|
-
console.print("[red]No valid options found in the response[/red]")
|
172
|
-
return
|
173
|
-
continue
|
174
|
-
|
175
|
-
if not validate_option_letter(option, options):
|
176
|
-
console = Console()
|
177
|
-
console.print(f"[red]Invalid option '{option}'. Valid options are: {', '.join(options.keys())} or 'M' to modify[/red]")
|
178
|
-
continue
|
179
|
-
|
180
|
-
break
|
181
|
-
|
182
|
-
paths_to_scan = [workdir] if workdir else []
|
183
|
-
if include:
|
184
|
-
paths_to_scan.extend(include)
|
185
|
-
files_content = collect_files_content(paths_to_scan, workdir) if paths_to_scan else ""
|
186
|
-
|
187
|
-
# Format the selected option before building prompt
|
188
|
-
selected_option = options[option]
|
189
|
-
option_text = format_option_text(selected_option)
|
190
|
-
|
191
|
-
# Remove initial_response from the arguments
|
192
|
-
selected_prompt = build_selected_option_prompt(option_text, request, files_content)
|
193
|
-
prompt_file = save_to_file(selected_prompt, 'selected', workdir)
|
194
|
-
if config.verbose:
|
195
|
-
print(f"\nSelected prompt saved to: {prompt_file}")
|
196
|
-
|
197
|
-
selected_response = progress_send_message(claude, selected_prompt)
|
198
|
-
changes_file = save_to_file(selected_response, 'changes', workdir)
|
199
|
-
|
200
|
-
if config.verbose:
|
201
|
-
try:
|
202
|
-
rel_path = changes_file.relative_to(workdir)
|
203
|
-
print(f"\nChanges saved to: ./{rel_path}")
|
204
|
-
except ValueError:
|
205
|
-
print(f"\nChanges saved to: {changes_file}")
|
206
|
-
|
207
|
-
changes = parse_block_changes(selected_response)
|
208
|
-
preview_and_apply_changes(changes, workdir, config.test_cmd)
|
209
|
-
|
210
|
-
def replay_saved_file(filepath: Path, claude: ClaudeAPIAgent, workdir: Path, raw: bool = False) -> None:
|
211
|
-
"""Process a saved prompt file and display the response"""
|
212
|
-
if not filepath.exists():
|
213
|
-
raise FileNotFoundError(f"File {filepath} not found")
|
214
|
-
|
215
|
-
content = filepath.read_text()
|
216
|
-
|
217
|
-
# Add debug output of file content
|
218
|
-
if config.debug:
|
219
|
-
console = Console()
|
220
|
-
console.print("\n[bold blue]Debug: File Content[/bold blue]")
|
221
|
-
console.print(Panel(
|
222
|
-
content,
|
223
|
-
title=f"Content of {filepath.name}",
|
224
|
-
border_style="blue",
|
225
|
-
padding=(1, 2)
|
226
|
-
))
|
227
|
-
console.print()
|
228
|
-
|
229
|
-
file_type = get_history_file_type(filepath)
|
230
|
-
|
231
|
-
if file_type == 'changes':
|
232
|
-
changes = parse_block_changes(content)
|
233
|
-
success = preview_and_apply_changes(changes, workdir, config.test_cmd)
|
234
|
-
if not success:
|
23
|
+
Returns:
|
24
|
+
Validated list of paths or None if no paths provided
|
25
|
+
"""
|
26
|
+
if not paths: # This handles both None and empty list cases
|
27
|
+
return None
|
28
|
+
|
29
|
+
# Convert paths to absolute and resolve symlinks
|
30
|
+
resolved_paths: Set[Path] = set()
|
31
|
+
unique_paths: List[Path] = []
|
32
|
+
|
33
|
+
for path in paths:
|
34
|
+
resolved = path.absolute().resolve()
|
35
|
+
if resolved in resolved_paths:
|
36
|
+
error_text = Text(f"\nError: Duplicate path provided: {path} ", style="red")
|
37
|
+
rich_print(error_text)
|
235
38
|
raise typer.Exit(1)
|
236
|
-
|
237
|
-
|
238
|
-
handle_option_selection(claude, content, content, raw, workdir)
|
239
|
-
elif file_type == 'selected':
|
240
|
-
if raw:
|
241
|
-
console = Console()
|
242
|
-
console.print("\n=== Prompt Content ===")
|
243
|
-
console.print(content)
|
244
|
-
console.print("=== End Prompt Content ===\n")
|
245
|
-
|
246
|
-
response = progress_send_message(claude, content)
|
247
|
-
changes_file = save_to_file(response, 'changes_', workdir)
|
248
|
-
print(f"\nChanges saved to: {changes_file}")
|
249
|
-
|
250
|
-
changes = parse_block_changes(response)
|
251
|
-
preview_and_apply_changes(changes, workdir, config.test_cmd)
|
252
|
-
else:
|
253
|
-
response = progress_send_message(claude, content)
|
254
|
-
format_analysis(response, raw)
|
255
|
-
|
256
|
-
def process_question(question: str, workdir: Path, include: List[Path], raw: bool, claude: ClaudeAPIAgent) -> None:
|
257
|
-
"""Process a question about the codebase"""
|
258
|
-
paths_to_scan = [workdir] if workdir else []
|
259
|
-
if include:
|
260
|
-
paths_to_scan.extend(include)
|
261
|
-
files_content = collect_files_content(paths_to_scan, workdir)
|
262
|
-
answer = ask_question(question, files_content, claude)
|
263
|
-
display_answer(answer, raw)
|
39
|
+
resolved_paths.add(resolved)
|
40
|
+
unique_paths.append(path)
|
264
41
|
|
265
|
-
|
266
|
-
"""Ensure working directory exists, prompt for creation if it doesn't"""
|
267
|
-
if workdir.exists():
|
268
|
-
return workdir
|
269
|
-
|
270
|
-
console = Console()
|
271
|
-
console.print(f"\n[yellow]Directory does not exist:[/yellow] {workdir}")
|
272
|
-
if Confirm.ask("Create directory?"):
|
273
|
-
workdir.mkdir(parents=True)
|
274
|
-
console.print(f"[green]Created directory:[/green] {workdir}")
|
275
|
-
return workdir
|
276
|
-
raise typer.Exit(1)
|
42
|
+
return unique_paths if unique_paths else None
|
277
43
|
|
278
44
|
def typer_main(
|
279
|
-
|
45
|
+
change_request: str = typer.Argument(None, help="Change request or command"),
|
46
|
+
workspace_dir: Optional[Path] = typer.Option(None, "-w", "--workspace_dir", help="Working directory", file_okay=False, dir_okay=True),
|
47
|
+
debug: bool = typer.Option(False, "--debug", help="Show debug information"),
|
48
|
+
verbose: bool = typer.Option(False, "--verbose", help="Show verbose output"),
|
49
|
+
include: Optional[List[Path]] = typer.Option(None, "-i", "--include", help="Additional paths to include"),
|
280
50
|
ask: Optional[str] = typer.Option(None, "--ask", help="Ask a question about the codebase"),
|
281
|
-
workdir: Optional[Path] = typer.Option(None, "-w", "--workdir",
|
282
|
-
help="Working directory (defaults to current directory)",
|
283
|
-
file_okay=False, dir_okay=True),
|
284
|
-
raw: bool = typer.Option(False, "--raw", help="Print raw response instead of markdown format"),
|
285
51
|
play: Optional[Path] = typer.Option(None, "--play", help="Replay a saved prompt file"),
|
286
|
-
include: Optional[List[Path]] = typer.Option(None, "-i", "--include", help="Additional paths to include in analysis", exists=True),
|
287
|
-
debug: bool = typer.Option(False, "--debug", help="Show debug information"),
|
288
|
-
verbose: bool = typer.Option(False, "-v", "--verbose", help="Show verbose output"),
|
289
52
|
scan: bool = typer.Option(False, "--scan", help="Preview files that would be analyzed"),
|
290
|
-
version: bool = typer.Option(False, "--version", help="Show version
|
291
|
-
|
292
|
-
|
293
|
-
"""
|
294
|
-
|
295
|
-
"""
|
296
|
-
|
53
|
+
version: bool = typer.Option(False, "--version", help="Show version information"),
|
54
|
+
test_cmd: Optional[str] = typer.Option(None, "--test", help="Command to run tests after changes"),
|
55
|
+
auto_apply: bool = typer.Option(False, "--auto-apply", help="Apply changes without confirmation"),
|
56
|
+
tui: bool = typer.Option(False, "--tui", help="Use terminal user interface"),
|
57
|
+
history: bool = typer.Option(False, "--history", help="Display history of requests"),
|
58
|
+
recursive: Optional[List[Path]] = typer.Option(None, "-r", "--recursive", help="Paths to scan recursively (directories only)"),
|
59
|
+
demo: bool = typer.Option(False, "--demo", help="Run demo scenarios"),
|
60
|
+
skipwork: bool = typer.Option(False, "--skipwork", help="Skip scanning workspace_dir when using include paths"),
|
61
|
+
):
|
62
|
+
"""Janito - AI-powered code modification assistant"""
|
297
63
|
if version:
|
298
64
|
console = Console()
|
299
|
-
console.print(f"Janito
|
300
|
-
raise typer.Exit()
|
301
|
-
|
302
|
-
config.set_debug(debug)
|
303
|
-
config.set_verbose(verbose)
|
304
|
-
config.set_test_cmd(test)
|
305
|
-
|
306
|
-
claude = ClaudeAPIAgent(system_prompt=SYSTEM_PROMPT)
|
307
|
-
|
308
|
-
if not any([request, ask, play, scan]):
|
309
|
-
workdir = workdir or Path.cwd()
|
310
|
-
workdir = ensure_workdir(workdir)
|
311
|
-
from janito.console import start_console_session
|
312
|
-
start_console_session(workdir, include)
|
65
|
+
console.print(f"Janito version {get_version()}")
|
313
66
|
return
|
314
67
|
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
include = [
|
320
|
-
path if path.is_absolute() else (workdir / path).resolve()
|
321
|
-
for path in include
|
322
|
-
]
|
323
|
-
|
324
|
-
if ask:
|
325
|
-
process_question(ask, workdir, include, raw, claude)
|
68
|
+
if demo:
|
69
|
+
from janito.cli.handlers.demo import DemoHandler
|
70
|
+
handler = DemoHandler()
|
71
|
+
handler.handle()
|
326
72
|
return
|
327
73
|
|
328
|
-
if
|
329
|
-
|
330
|
-
|
74
|
+
if history:
|
75
|
+
from janito.cli.history import display_history
|
76
|
+
display_history()
|
331
77
|
return
|
332
78
|
|
333
|
-
|
334
|
-
|
335
|
-
|
79
|
+
config.set_workspace_dir(workspace_dir)
|
80
|
+
config.set_debug(debug)
|
81
|
+
config.set_verbose(verbose)
|
82
|
+
config.set_auto_apply(auto_apply)
|
83
|
+
config.set_include(include)
|
84
|
+
config.set_tui(tui)
|
85
|
+
config.set_skipwork(skipwork)
|
336
86
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
87
|
+
# Validate skipwork usage
|
88
|
+
if skipwork and not include and not recursive:
|
89
|
+
error_text = Text("\nError: --skipwork requires at least one include path (-i or -r)", style="red")
|
90
|
+
rich_print(error_text)
|
91
|
+
raise typer.Exit(1)
|
92
|
+
|
93
|
+
if include:
|
94
|
+
resolved_paths = []
|
95
|
+
for path in include:
|
96
|
+
path = config.workspace_dir / path
|
97
|
+
resolved_paths.append(path.resolve())
|
98
|
+
config.set_include(resolved_paths)
|
99
|
+
|
100
|
+
# Validate recursive paths
|
101
|
+
if recursive:
|
102
|
+
resolved_paths = []
|
103
|
+
for path in recursive:
|
104
|
+
final_path = config.workspace_dir / path
|
105
|
+
if not path.is_dir():
|
106
|
+
error_text = Text(f"\nError: Recursive path must be a directory: {path} ", style="red")
|
107
|
+
rich_print(error_text)
|
108
|
+
raise typer.Exit(1)
|
109
|
+
resolved_paths.append(final_path.resolve())
|
110
|
+
config.set_recursive(resolved_paths)
|
111
|
+
include = include or []
|
112
|
+
include.extend(resolved_paths)
|
113
|
+
config.set_include(include)
|
114
|
+
|
115
|
+
if test_cmd:
|
116
|
+
config.set_test_cmd(test_cmd)
|
117
|
+
|
118
|
+
if ask:
|
119
|
+
handle_ask(ask)
|
120
|
+
elif play:
|
121
|
+
handle_play(play)
|
122
|
+
elif scan:
|
123
|
+
paths_to_scan = include or [config.workspace_dir]
|
124
|
+
handle_scan(paths_to_scan)
|
125
|
+
elif change_request:
|
126
|
+
handle_request(change_request)
|
344
127
|
else:
|
345
|
-
|
346
|
-
|
347
|
-
initial_prompt = build_request_analysis_prompt(files_content, request)
|
348
|
-
initial_response = progress_send_message(claude, initial_prompt)
|
349
|
-
save_to_file(initial_response, 'analysis', workdir)
|
350
|
-
|
351
|
-
format_analysis(initial_response, raw, claude)
|
352
|
-
|
353
|
-
handle_option_selection(claude, initial_response, request, raw, workdir, include)
|
128
|
+
from janito.shell import start_shell
|
129
|
+
start_shell()
|
354
130
|
|
355
131
|
def main():
|
356
132
|
typer.run(typer_main)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
SYSTEM_PROMPT = """I am Janito, your friendly software development buddy. I help you with coding tasks while being clear and concise in my responses."""
|
4
|
+
|
5
|
+
ai_backend = os.getenv('AI_BACKEND', 'claudeai').lower()
|
6
|
+
|
7
|
+
if ai_backend == 'openai':
|
8
|
+
from .openai import OpenAIAgent as AIAgent
|
9
|
+
elif ai_backend == 'claudeai':
|
10
|
+
from .claudeai import ClaudeAIAgent as AIAgent
|
11
|
+
else:
|
12
|
+
raise ValueError(f"Unsupported AI_BACKEND: {ai_backend}")
|
13
|
+
|
14
|
+
# Create a singleton instance
|
15
|
+
agent = AIAgent(SYSTEM_PROMPT)
|
16
|
+
|
janito/agents/agent.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from threading import Event
|
4
|
+
from typing import Optional, List, Tuple
|
5
|
+
|
6
|
+
class Agent(ABC):
|
7
|
+
"""Abstract base class for AI agents"""
|
8
|
+
def __init__(self, api_key: Optional[str] = None, system_prompt: str = None):
|
9
|
+
self.api_key = api_key
|
10
|
+
self.system_message = system_prompt
|
11
|
+
self.last_prompt = None
|
12
|
+
self.last_full_message = None
|
13
|
+
self.last_response = None
|
14
|
+
self.messages_history: List[Tuple[str, str]] = []
|
15
|
+
if system_prompt:
|
16
|
+
self.messages_history.append(("system", system_prompt))
|
17
|
+
|
18
|
+
@abstractmethod
|
19
|
+
def send_message(self, message: str, stop_event: Event = None) -> str:
|
20
|
+
"""Send message to AI service and return response"""
|
21
|
+
pass
|
@@ -2,24 +2,27 @@ import anthropic
|
|
2
2
|
import os
|
3
3
|
from typing import Optional
|
4
4
|
from threading import Event
|
5
|
+
from .agent import Agent
|
5
6
|
|
6
|
-
class
|
7
|
+
class ClaudeAIAgent(Agent):
|
7
8
|
"""Handles interaction with Claude API, including message handling"""
|
8
|
-
|
9
|
+
DEFAULT_MODEL = "claude-3-5-sonnet-20241022"
|
10
|
+
|
11
|
+
def __init__(self, system_prompt: str = None):
|
12
|
+
self.api_key = os.getenv('ANTHROPIC_API_KEY')
|
13
|
+
super().__init__(self.api_key, system_prompt)
|
9
14
|
if not system_prompt:
|
10
15
|
raise ValueError("system_prompt is required")
|
11
|
-
|
16
|
+
|
12
17
|
if not self.api_key:
|
13
18
|
raise ValueError("ANTHROPIC_API_KEY environment variable is required")
|
14
19
|
self.client = anthropic.Client(api_key=self.api_key)
|
15
|
-
self.model =
|
20
|
+
self.model = os.getenv('CLAUDE_MODEL', self.DEFAULT_MODEL)
|
16
21
|
self.system_message = system_prompt
|
17
22
|
self.last_prompt = None
|
18
23
|
self.last_full_message = None
|
19
24
|
self.last_response = None
|
20
|
-
|
21
|
-
if system_prompt:
|
22
|
-
self.messages_history.append(("system", system_prompt))
|
25
|
+
|
23
26
|
|
24
27
|
def send_message(self, message: str, stop_event: Event = None) -> str:
|
25
28
|
"""Send message to Claude API and return response"""
|
@@ -35,23 +38,16 @@ class ClaudeAPIAgent:
|
|
35
38
|
response = self.client.messages.create(
|
36
39
|
model=self.model, # Use discovered model
|
37
40
|
system=self.system_message,
|
38
|
-
max_tokens=
|
41
|
+
max_tokens=8192,
|
39
42
|
messages=[
|
40
43
|
{"role": "user", "content": message}
|
41
44
|
],
|
42
45
|
temperature=0,
|
43
46
|
)
|
44
47
|
|
45
|
-
|
46
|
-
response_text = response.content[0].text
|
47
|
-
|
48
|
-
# Only store and process response if not cancelled
|
49
|
-
if not (stop_event and stop_event.is_set()):
|
50
|
-
self.last_response = response_text
|
51
|
-
self.messages_history.append(("assistant", response_text))
|
52
|
-
|
48
|
+
|
53
49
|
# Always return the response, let caller handle cancellation
|
54
|
-
return
|
50
|
+
return response
|
55
51
|
|
56
52
|
except KeyboardInterrupt:
|
57
53
|
if stop_event:
|
janito/agents/openai.py
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
import openai # updated import
|
2
|
+
import os
|
3
|
+
from typing import Optional
|
4
|
+
from threading import Event
|
5
|
+
from .agent import Agent
|
6
|
+
|
7
|
+
class OpenAIAgent(Agent):
|
8
|
+
"""Handles interaction with OpenAI API, including message handling"""
|
9
|
+
DEFAULT_MODEL = "o1-mini-2024-09-12"
|
10
|
+
|
11
|
+
def __init__(self, api_key: Optional[str] = None, system_prompt: str = None):
|
12
|
+
super().__init__(api_key, system_prompt)
|
13
|
+
if not system_prompt:
|
14
|
+
raise ValueError("system_prompt is required")
|
15
|
+
self.api_key = api_key or os.getenv('OPENAI_API_KEY')
|
16
|
+
if not self.api_key:
|
17
|
+
raise ValueError("OPENAI_API_KEY environment variable is required")
|
18
|
+
openai.api_key = self.api_key
|
19
|
+
openai.organization = os.getenv("OPENAI_ORG")
|
20
|
+
self.client = openai.Client() # initialized client
|
21
|
+
self.model = os.getenv('OPENAI_MODEL', "o1-mini-2024-09-12") # reverted to original default model
|
22
|
+
|
23
|
+
def send_message(self, message: str, stop_event: Event = None) -> str:
|
24
|
+
"""Send message to OpenAI API and return response"""
|
25
|
+
self.messages_history.append(("user", message))
|
26
|
+
self.last_full_message = message
|
27
|
+
|
28
|
+
try:
|
29
|
+
if stop_event and stop_event.is_set():
|
30
|
+
return ""
|
31
|
+
|
32
|
+
#messages = [{"role": "system", "content": self.system_message}]
|
33
|
+
messages = [{"role": "user", "content": message}]
|
34
|
+
|
35
|
+
response = self.client.chat.completions.create(
|
36
|
+
model=self.model,
|
37
|
+
messages=messages,
|
38
|
+
max_completion_tokens=4000,
|
39
|
+
temperature=1,
|
40
|
+
)
|
41
|
+
|
42
|
+
response_text = response.choices[0].message.content
|
43
|
+
|
44
|
+
if not (stop_event and stop_event.is_set()):
|
45
|
+
self.last_response = response_text
|
46
|
+
self.messages_history.append(("assistant", response_text))
|
47
|
+
|
48
|
+
return response_text
|
49
|
+
|
50
|
+
except KeyboardInterrupt:
|
51
|
+
if stop_event:
|
52
|
+
stop_event.set()
|
53
|
+
return ""
|
janito/agents/test.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
import unittest
|
2
|
+
import os
|
3
|
+
from unittest.mock import patch, MagicMock
|
4
|
+
from .openai import OpenAIAgent
|
5
|
+
from .claudeai import AIAgent
|
6
|
+
|
7
|
+
class TestAIAgents(unittest.TestCase):
|
8
|
+
def setUp(self):
|
9
|
+
self.system_prompt = "You are a helpful assistant."
|
10
|
+
self.test_message = "Hello, how are you?"
|
11
|
+
|
12
|
+
def test_openai_agent_initialization(self):
|
13
|
+
with patch.dict(os.environ, {'OPENAI_API_KEY': 'test_key'}):
|
14
|
+
agent = OpenAIAgent(system_prompt=self.system_prompt)
|
15
|
+
|
16
|
+
def test_claudeai_agent_initialization(self):
|
17
|
+
with patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test_key'}):
|
18
|
+
agent = AIAgent(system_prompt=self.system_prompt)
|
19
|
+
|
20
|
+
def test_openai_agent_send_message(self):
|
21
|
+
with patch('openai.OpenAI.chat.completions.create') as mock_create:
|
22
|
+
mock_response = MagicMock()
|
23
|
+
mock_response.choices[0].message.content = "I'm good, thank you!"
|
24
|
+
mock_create.return_value = mock_response
|
25
|
+
response = self.openai_agent.send_message(self.test_message)
|
26
|
+
self.assertEqual(response, "I'm good, thank you!")
|
27
|
+
|
28
|
+
def test_claudeai_agent_send_message(self):
|
29
|
+
with patch('anthropic.Client.messages.create') as mock_create:
|
30
|
+
mock_response = MagicMock()
|
31
|
+
mock_response.content[0].text = "I'm Claude, how can I assist you?"
|
32
|
+
mock_create.return_value = mock_response
|
33
|
+
response = self.claudeai_agent.send_message(self.test_message)
|
34
|
+
self.assertEqual(response, "I'm Claude, how can I assist you?")
|