janito 0.4.0__py3-none-any.whl → 0.5.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 +48 -1
- janito/__main__.py +29 -334
- janito/agents/__init__.py +22 -0
- janito/agents/agent.py +21 -0
- janito/{claude.py → agents/claudeai.py} +10 -5
- janito/agents/openai.py +53 -0
- janito/agents/test.py +34 -0
- janito/analysis/__init__.py +33 -0
- janito/analysis/display.py +149 -0
- janito/analysis/options.py +112 -0
- janito/analysis/prompts.py +75 -0
- janito/change/__init__.py +19 -0
- janito/change/applier.py +269 -0
- janito/{contentchange.py → change/content.py} +5 -27
- janito/change/indentation.py +33 -0
- janito/change/position.py +169 -0
- janito/changehistory.py +46 -0
- janito/changeviewer/__init__.py +12 -0
- janito/changeviewer/diff.py +28 -0
- janito/changeviewer/panels.py +268 -0
- janito/changeviewer/styling.py +59 -0
- janito/changeviewer/themes.py +57 -0
- janito/cli/__init__.py +2 -0
- janito/cli/commands.py +53 -0
- janito/cli/functions.py +286 -0
- janito/cli/registry.py +26 -0
- janito/common.py +9 -9
- janito/console/__init__.py +3 -0
- janito/console/commands.py +112 -0
- janito/console/core.py +62 -0
- janito/console/display.py +157 -0
- janito/fileparser.py +292 -83
- janito/prompts.py +21 -6
- janito/qa.py +7 -5
- janito/review.py +13 -0
- janito/scan.py +44 -5
- janito/tests/test_fileparser.py +26 -0
- janito-0.5.0.dist-info/METADATA +146 -0
- janito-0.5.0.dist-info/RECORD +45 -0
- janito/analysis.py +0 -281
- janito/changeapplier.py +0 -436
- janito/changeviewer.py +0 -350
- janito/console.py +0 -330
- janito-0.4.0.dist-info/METADATA +0 -164
- janito-0.4.0.dist-info/RECORD +0 -21
- /janito/{contextparser.py → _contextparser.py} +0 -0
- {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/WHEEL +0 -0
- {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/entry_points.txt +0 -0
- {janito-0.4.0.dist-info → janito-0.5.0.dist-info}/licenses/LICENSE +0 -0
janito/__init__.py
CHANGED
@@ -1,2 +1,49 @@
|
|
1
|
+
"""Core package initialization for Janito."""
|
1
2
|
|
2
|
-
|
3
|
+
from .analysis import (
|
4
|
+
AnalysisOption,
|
5
|
+
parse_analysis_options,
|
6
|
+
format_analysis,
|
7
|
+
get_history_file_type,
|
8
|
+
get_history_path,
|
9
|
+
get_timestamp,
|
10
|
+
save_to_file,
|
11
|
+
build_request_analysis_prompt,
|
12
|
+
get_option_selection,
|
13
|
+
prompt_user,
|
14
|
+
validate_option_letter
|
15
|
+
)
|
16
|
+
|
17
|
+
from .change import (
|
18
|
+
apply_single_change,
|
19
|
+
parse_and_apply_changes_sequence,
|
20
|
+
get_file_type,
|
21
|
+
process_and_save_changes,
|
22
|
+
format_parsed_changes,
|
23
|
+
apply_content_changes,
|
24
|
+
handle_changes_file
|
25
|
+
)
|
26
|
+
|
27
|
+
__all__ = [
|
28
|
+
# Analysis exports
|
29
|
+
'AnalysisOption',
|
30
|
+
'parse_analysis_options',
|
31
|
+
'format_analysis',
|
32
|
+
'get_history_file_type',
|
33
|
+
'get_history_path',
|
34
|
+
'get_timestamp',
|
35
|
+
'save_to_file',
|
36
|
+
'build_request_analysis_prompt',
|
37
|
+
'get_option_selection',
|
38
|
+
'prompt_user',
|
39
|
+
'validate_option_letter',
|
40
|
+
|
41
|
+
# Change exports
|
42
|
+
'apply_single_change',
|
43
|
+
'parse_and_apply_changes_sequence',
|
44
|
+
'get_file_type',
|
45
|
+
'process_and_save_changes',
|
46
|
+
'format_parsed_changes',
|
47
|
+
'apply_content_changes',
|
48
|
+
'handle_changes_file'
|
49
|
+
]
|
janito/__main__.py
CHANGED
@@ -1,359 +1,54 @@
|
|
1
|
-
import sys
|
2
1
|
import typer
|
3
|
-
from typing import Optional,
|
2
|
+
from typing import Optional, List
|
4
3
|
from pathlib import Path
|
5
|
-
from janito.claude import ClaudeAPIAgent
|
6
|
-
import shutil
|
7
|
-
from janito.prompts import (
|
8
|
-
build_selected_option_prompt,
|
9
|
-
SYSTEM_PROMPT,
|
10
|
-
)
|
11
4
|
from rich.console import Console
|
12
|
-
from
|
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
|
-
from rich.text import Text
|
28
|
-
from rich.rule import Rule
|
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']}]")
|
61
|
-
|
62
|
-
def validate_option_letter(letter: str, options: dict) -> bool:
|
63
|
-
"""Validate if the given letter is a valid option or 'M' for modify"""
|
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')
|
85
|
-
|
86
|
-
def save_prompt_to_file(prompt: str) -> Path:
|
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
|
92
|
-
|
93
|
-
def save_to_file(content: str, prefix: str, workdir: Path) -> Path:
|
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
|
101
|
-
|
102
|
-
def modify_request(request: str) -> str:
|
103
|
-
"""Display current request and get modified version with improved formatting"""
|
104
|
-
console = Console()
|
105
|
-
|
106
|
-
# Display current request in a panel with clear formatting
|
107
|
-
console.print("\n[bold cyan]Current Request:[/bold cyan]")
|
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()
|
150
|
-
|
151
|
-
if option == 'M':
|
152
|
-
# Use the new modify_request function for better UX
|
153
|
-
new_request = modify_request(request)
|
154
|
-
if new_request == request:
|
155
|
-
continue
|
156
|
-
|
157
|
-
# Rerun analysis with new request
|
158
|
-
paths_to_scan = [workdir] if workdir else []
|
159
|
-
if include:
|
160
|
-
paths_to_scan.extend(include)
|
161
|
-
files_content = collect_files_content(paths_to_scan, workdir) if paths_to_scan else ""
|
162
|
-
|
163
|
-
initial_prompt = build_request_analysis_prompt(files_content, new_request)
|
164
|
-
initial_response = progress_send_message(claude, initial_prompt)
|
165
|
-
save_to_file(initial_response, 'analysis', workdir)
|
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()
|
5
|
+
from .version import get_version
|
228
6
|
|
229
|
-
|
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:
|
235
|
-
raise typer.Exit(1)
|
236
|
-
elif file_type == 'analysis':
|
237
|
-
format_analysis(content, raw, claude)
|
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)
|
7
|
+
from janito.agents import AgentSingleton
|
8
|
+
from janito.config import config
|
255
9
|
|
256
|
-
|
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)
|
10
|
+
from .cli.commands import handle_request, handle_ask, handle_play, handle_scan
|
264
11
|
|
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)
|
12
|
+
app = typer.Typer(add_completion=False)
|
277
13
|
|
278
14
|
def typer_main(
|
279
|
-
|
15
|
+
change_request: str = typer.Argument(None, help="Change request or command"),
|
16
|
+
workdir: Optional[Path] = typer.Option(None, "-w", "--workdir", help="Working directory", file_okay=False, dir_okay=True),
|
17
|
+
debug: bool = typer.Option(False, "--debug", help="Show debug information"),
|
18
|
+
verbose: bool = typer.Option(False, "--verbose", help="Show verbose output"),
|
19
|
+
include: Optional[List[Path]] = typer.Option(None, "-i", "--include", help="Additional paths to include"),
|
280
20
|
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
21
|
play: Optional[Path] = typer.Option(None, "--play", help="Replay a saved prompt file"),
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
scan: bool = typer.Option(False, "--scan", help="Preview files that would be analyzed"),
|
290
|
-
version: bool = typer.Option(False, "--version", help="Show version and exit"),
|
291
|
-
test: Optional[str] = typer.Option(None, "-t", "--test", help="Test command to run before applying changes"),
|
292
|
-
) -> None:
|
293
|
-
"""
|
294
|
-
Analyze files and provide modification instructions.
|
295
|
-
"""
|
296
|
-
|
22
|
+
version: bool = typer.Option(False, "--version", help="Show version information"),
|
23
|
+
):
|
24
|
+
"""Janito - AI-powered code modification assistant"""
|
297
25
|
if version:
|
298
26
|
console = Console()
|
299
|
-
console.print(f"Janito
|
300
|
-
|
27
|
+
console.print(f"Janito version {get_version()}")
|
28
|
+
return
|
301
29
|
|
30
|
+
workdir = workdir or Path.cwd()
|
302
31
|
config.set_debug(debug)
|
303
32
|
config.set_verbose(verbose)
|
304
|
-
config.set_test_cmd(test)
|
305
33
|
|
306
|
-
|
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)
|
313
|
-
return
|
34
|
+
agent = AgentSingleton.get_agent()
|
314
35
|
|
315
|
-
workdir = workdir or Path.cwd()
|
316
|
-
workdir = ensure_workdir(workdir)
|
317
|
-
|
318
|
-
if include:
|
319
|
-
include = [
|
320
|
-
path if path.is_absolute() else (workdir / path).resolve()
|
321
|
-
for path in include
|
322
|
-
]
|
323
|
-
|
324
36
|
if ask:
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
37
|
+
handle_ask(ask, workdir, include, False, agent)
|
38
|
+
elif play:
|
39
|
+
handle_play(play, workdir, False)
|
40
|
+
elif change_request == "scan":
|
329
41
|
paths_to_scan = include if include else [workdir]
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
if play:
|
334
|
-
replay_saved_file(play, claude, workdir, raw)
|
335
|
-
return
|
336
|
-
|
337
|
-
paths_to_scan = include if include else [workdir]
|
338
|
-
|
339
|
-
is_empty = is_dir_empty(workdir)
|
340
|
-
if is_empty and not include:
|
341
|
-
console = Console()
|
342
|
-
console.print("\n[bold blue]Empty directory - will create new files as needed[/bold blue]")
|
343
|
-
files_content = ""
|
42
|
+
handle_scan(paths_to_scan, workdir)
|
43
|
+
elif change_request:
|
44
|
+
handle_request(change_request, workdir, include, False, agent)
|
344
45
|
else:
|
345
|
-
|
346
|
-
|
347
|
-
|
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)
|
46
|
+
console = Console()
|
47
|
+
console.print("Error: Please provide a change request or use --ask/--play options")
|
48
|
+
raise typer.Exit(1)
|
354
49
|
|
355
50
|
def main():
|
356
51
|
typer.run(typer_main)
|
357
52
|
|
358
53
|
if __name__ == "__main__":
|
359
|
-
main()
|
54
|
+
main()
|
@@ -0,0 +1,22 @@
|
|
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
|
+
class AgentSingleton:
|
15
|
+
_instance = None
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
def get_agent(cls):
|
19
|
+
if cls._instance is None:
|
20
|
+
cls._instance = AIAgent(SYSTEM_PROMPT)
|
21
|
+
return cls._instance
|
22
|
+
|
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,17 +2,22 @@ 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
|
@@ -35,7 +40,7 @@ class ClaudeAPIAgent:
|
|
35
40
|
response = self.client.messages.create(
|
36
41
|
model=self.model, # Use discovered model
|
37
42
|
system=self.system_message,
|
38
|
-
max_tokens=
|
43
|
+
max_tokens=8192,
|
39
44
|
messages=[
|
40
45
|
{"role": "user", "content": message}
|
41
46
|
],
|
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?")
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"""Analysis module for Janito.
|
2
|
+
|
3
|
+
This module provides functionality for analyzing and displaying code changes.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from .options import AnalysisOption, parse_analysis_options
|
7
|
+
from .display import (
|
8
|
+
format_analysis,
|
9
|
+
get_history_file_type,
|
10
|
+
get_history_path,
|
11
|
+
get_timestamp,
|
12
|
+
save_to_file
|
13
|
+
)
|
14
|
+
from .prompts import (
|
15
|
+
build_request_analysis_prompt,
|
16
|
+
get_option_selection,
|
17
|
+
prompt_user,
|
18
|
+
validate_option_letter
|
19
|
+
)
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
'AnalysisOption',
|
23
|
+
'parse_analysis_options',
|
24
|
+
'format_analysis',
|
25
|
+
'get_history_file_type',
|
26
|
+
'get_history_path',
|
27
|
+
'get_timestamp',
|
28
|
+
'save_to_file',
|
29
|
+
'build_request_analysis_prompt',
|
30
|
+
'get_option_selection',
|
31
|
+
'prompt_user',
|
32
|
+
'validate_option_letter'
|
33
|
+
]
|