janito 0.3.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 -235
- janito/_contextparser.py +113 -0
- janito/agents/__init__.py +22 -0
- janito/agents/agent.py +21 -0
- janito/agents/claudeai.py +64 -0
- 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/change/content.py +62 -0
- 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 +23 -0
- janito/config.py +8 -3
- 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 +334 -0
- janito/prompts.py +58 -74
- janito/qa.py +40 -7
- janito/review.py +13 -0
- janito/scan.py +68 -14
- janito/tests/test_fileparser.py +26 -0
- janito/version.py +23 -0
- janito-0.5.0.dist-info/METADATA +146 -0
- janito-0.5.0.dist-info/RECORD +45 -0
- janito/changeviewer.py +0 -64
- janito/claude.py +0 -74
- janito/console.py +0 -60
- janito/contentchange.py +0 -165
- janito-0.3.0.dist-info/METADATA +0 -138
- janito-0.3.0.dist-info/RECORD +0 -15
- {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/WHEEL +0 -0
- {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/entry_points.txt +0 -0
- {janito-0.3.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,260 +1,54 @@
|
|
1
1
|
import typer
|
2
|
-
from typing import Optional,
|
2
|
+
from typing import Optional, List
|
3
3
|
from pathlib import Path
|
4
|
-
from janito.claude import ClaudeAPIAgent
|
5
|
-
import shutil
|
6
|
-
from janito.prompts import (
|
7
|
-
build_request_analisys_prompt,
|
8
|
-
build_selected_option_prompt,
|
9
|
-
SYSTEM_PROMPT,
|
10
|
-
parse_options
|
11
|
-
)
|
12
4
|
from rich.console import Console
|
13
|
-
from
|
14
|
-
import re
|
15
|
-
import tempfile
|
16
|
-
import json
|
17
|
-
from rich.syntax import Syntax
|
18
|
-
from janito.contentchange import (
|
19
|
-
handle_changes_file,
|
20
|
-
get_file_type,
|
21
|
-
parse_block_changes,
|
22
|
-
preview_and_apply_changes,
|
23
|
-
format_parsed_changes,
|
24
|
-
)
|
25
|
-
from rich.table import Table
|
26
|
-
from rich.columns import Columns
|
27
|
-
from rich.panel import Panel
|
28
|
-
from rich.text import Text
|
29
|
-
from rich.rule import Rule
|
30
|
-
from rich import box
|
31
|
-
from datetime import datetime, timezone
|
32
|
-
from itertools import chain
|
33
|
-
from janito.scan import collect_files_content, is_dir_empty, preview_scan
|
34
|
-
from janito.qa import ask_question, display_answer
|
35
|
-
from rich.prompt import Prompt, Confirm
|
36
|
-
from janito.config import config
|
37
|
-
from importlib.metadata import version
|
38
|
-
|
39
|
-
def get_version() -> str:
|
40
|
-
try:
|
41
|
-
return version("janito")
|
42
|
-
except:
|
43
|
-
return "dev"
|
44
|
-
|
45
|
-
def format_analysis(analysis: str, raw: bool = False, claude: Optional[ClaudeAPIAgent] = None) -> None:
|
46
|
-
"""Format and display the analysis output"""
|
47
|
-
console = Console()
|
48
|
-
if raw and claude:
|
49
|
-
console.print("\n=== Message History ===")
|
50
|
-
for role, content in claude.messages_history:
|
51
|
-
console.print(f"\n[bold cyan]{role.upper()}:[/bold cyan]")
|
52
|
-
console.print(content)
|
53
|
-
console.print("\n=== End Message History ===\n")
|
54
|
-
else:
|
55
|
-
md = Markdown(analysis)
|
56
|
-
console.print(md)
|
57
|
-
|
58
|
-
def prompt_user(message: str, choices: List[str] = None) -> str:
|
59
|
-
"""Display a prominent user prompt with optional choices"""
|
60
|
-
console = Console()
|
61
|
-
console.print()
|
62
|
-
console.print(Rule(" User Input Required ", style="bold cyan"))
|
63
|
-
|
64
|
-
if choices:
|
65
|
-
choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
|
66
|
-
console.print(Panel(choice_text, box=box.ROUNDED))
|
67
|
-
|
68
|
-
return Prompt.ask(f"[bold cyan]> {message}[/bold cyan]")
|
69
|
-
|
70
|
-
def get_option_selection() -> int:
|
71
|
-
"""Get user input for option selection"""
|
72
|
-
while True:
|
73
|
-
try:
|
74
|
-
option = int(prompt_user("Select option number"))
|
75
|
-
return option
|
76
|
-
except ValueError:
|
77
|
-
console = Console()
|
78
|
-
console.print("[red]Please enter a valid number[/red]")
|
79
|
-
|
80
|
-
def get_history_path(workdir: Path) -> Path:
|
81
|
-
"""Create and return the history directory path"""
|
82
|
-
history_dir = workdir / '.janito' / 'history'
|
83
|
-
history_dir.mkdir(parents=True, exist_ok=True)
|
84
|
-
return history_dir
|
85
|
-
|
86
|
-
def get_timestamp() -> str:
|
87
|
-
"""Get current UTC timestamp in YMD_HMS format with leading zeros"""
|
88
|
-
return datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
5
|
+
from .version import get_version
|
89
6
|
|
90
|
-
|
91
|
-
|
92
|
-
temp_file = tempfile.NamedTemporaryFile(prefix='selected_', suffix='.txt', delete=False)
|
93
|
-
temp_path = Path(temp_file.name)
|
94
|
-
temp_path.write_text(prompt)
|
95
|
-
return temp_path
|
96
|
-
|
97
|
-
def save_to_file(content: str, prefix: str, workdir: Path) -> Path:
|
98
|
-
"""Save content to a timestamped file in history directory"""
|
99
|
-
history_dir = get_history_path(workdir)
|
100
|
-
timestamp = get_timestamp()
|
101
|
-
filename = f"{timestamp}_{prefix}.txt"
|
102
|
-
file_path = history_dir / filename
|
103
|
-
file_path.write_text(content)
|
104
|
-
return file_path
|
105
|
-
|
106
|
-
def handle_option_selection(claude: ClaudeAPIAgent, initial_response: str, request: str, raw: bool = False, workdir: Optional[Path] = None, include: Optional[List[Path]] = None) -> None:
|
107
|
-
"""Handle option selection and implementation details"""
|
108
|
-
option = get_option_selection()
|
109
|
-
paths_to_scan = [workdir] if workdir else []
|
110
|
-
if include:
|
111
|
-
paths_to_scan.extend(include)
|
112
|
-
files_content = collect_files_content(paths_to_scan, workdir) if paths_to_scan else ""
|
113
|
-
|
114
|
-
selected_prompt = build_selected_option_prompt(option, request, initial_response, files_content)
|
115
|
-
prompt_file = save_to_file(selected_prompt, 'selected', workdir)
|
116
|
-
if config.verbose:
|
117
|
-
print(f"\nSelected prompt saved to: {prompt_file}")
|
118
|
-
|
119
|
-
selected_response = claude.send_message(selected_prompt)
|
120
|
-
changes_file = save_to_file(selected_response, 'changes', workdir)
|
121
|
-
if config.verbose:
|
122
|
-
print(f"\nChanges saved to: {changes_file}")
|
123
|
-
|
124
|
-
changes = parse_block_changes(selected_response)
|
125
|
-
preview_and_apply_changes(changes, workdir)
|
126
|
-
|
127
|
-
def replay_saved_file(filepath: Path, claude: ClaudeAPIAgent, workdir: Path, raw: bool = False) -> None:
|
128
|
-
"""Process a saved prompt file and display the response"""
|
129
|
-
if not filepath.exists():
|
130
|
-
raise FileNotFoundError(f"File {filepath} not found")
|
131
|
-
|
132
|
-
file_type = get_file_type(filepath)
|
133
|
-
content = filepath.read_text()
|
134
|
-
|
135
|
-
if file_type == 'changes':
|
136
|
-
changes = parse_block_changes(content)
|
137
|
-
preview_and_apply_changes(changes, workdir)
|
138
|
-
elif file_type == 'analysis':
|
139
|
-
format_analysis(content, raw, claude)
|
140
|
-
handle_option_selection(claude, content, content, raw, workdir)
|
141
|
-
elif file_type == 'selected':
|
142
|
-
if raw:
|
143
|
-
console = Console()
|
144
|
-
console.print("\n=== Prompt Content ===")
|
145
|
-
console.print(content)
|
146
|
-
console.print("=== End Prompt Content ===\n")
|
147
|
-
response = claude.send_message(content)
|
148
|
-
changes_file = save_to_file(response, 'changes_', workdir)
|
149
|
-
print(f"\nChanges saved to: {changes_file}")
|
150
|
-
|
151
|
-
changes = parse_block_changes(response)
|
152
|
-
preview_and_apply_changes(preview_changes, workdir)
|
153
|
-
else:
|
154
|
-
response = claude.send_message(content)
|
155
|
-
format_analysis(response, raw)
|
156
|
-
|
157
|
-
def process_question(question: str, workdir: Path, include: List[Path], raw: bool, claude: ClaudeAPIAgent) -> None:
|
158
|
-
"""Process a question about the codebase"""
|
159
|
-
paths_to_scan = [workdir] if workdir else []
|
160
|
-
if include:
|
161
|
-
paths_to_scan.extend(include)
|
162
|
-
files_content = collect_files_content(paths_to_scan, workdir)
|
7
|
+
from janito.agents import AgentSingleton
|
8
|
+
from janito.config import config
|
163
9
|
|
164
|
-
|
165
|
-
display_answer(answer, raw)
|
10
|
+
from .cli.commands import handle_request, handle_ask, handle_play, handle_scan
|
166
11
|
|
167
|
-
|
168
|
-
"""Ensure working directory exists, prompt for creation if it doesn't"""
|
169
|
-
if workdir.exists():
|
170
|
-
return workdir
|
171
|
-
|
172
|
-
console = Console()
|
173
|
-
console.print(f"\n[yellow]Directory does not exist:[/yellow] {workdir}")
|
174
|
-
if Confirm.ask("Create directory?"):
|
175
|
-
workdir.mkdir(parents=True)
|
176
|
-
console.print(f"[green]Created directory:[/green] {workdir}")
|
177
|
-
return workdir
|
178
|
-
raise typer.Exit(1)
|
12
|
+
app = typer.Typer(add_completion=False)
|
179
13
|
|
180
14
|
def typer_main(
|
181
|
-
|
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"),
|
182
20
|
ask: Optional[str] = typer.Option(None, "--ask", help="Ask a question about the codebase"),
|
183
|
-
workdir: Optional[Path] = typer.Option(None, "-w", "--workdir",
|
184
|
-
help="Working directory (defaults to current directory)",
|
185
|
-
file_okay=False, dir_okay=True),
|
186
|
-
raw: bool = typer.Option(False, "--raw", help="Print raw response instead of markdown format"),
|
187
21
|
play: Optional[Path] = typer.Option(None, "--play", help="Replay a saved prompt file"),
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
verbose: bool = typer.Option(False, "-v", "--verbose", help="Show verbose output"),
|
192
|
-
scan: bool = typer.Option(False, "--scan", help="Preview files that would be analyzed"),
|
193
|
-
version: bool = typer.Option(False, "--version", help="Show version and exit"),
|
194
|
-
) -> None:
|
195
|
-
"""
|
196
|
-
Analyze files and provide modification instructions.
|
197
|
-
"""
|
22
|
+
version: bool = typer.Option(False, "--version", help="Show version information"),
|
23
|
+
):
|
24
|
+
"""Janito - AI-powered code modification assistant"""
|
198
25
|
if version:
|
199
26
|
console = Console()
|
200
|
-
console.print(f"Janito
|
201
|
-
|
27
|
+
console.print(f"Janito version {get_version()}")
|
28
|
+
return
|
202
29
|
|
30
|
+
workdir = workdir or Path.cwd()
|
203
31
|
config.set_debug(debug)
|
204
32
|
config.set_verbose(verbose)
|
205
|
-
config.set_debug_line(debug_line)
|
206
33
|
|
207
|
-
|
34
|
+
agent = AgentSingleton.get_agent()
|
208
35
|
|
209
|
-
if not any([request, ask, play, scan]):
|
210
|
-
workdir = workdir or Path.cwd()
|
211
|
-
workdir = ensure_workdir(workdir)
|
212
|
-
from janito.console import start_console_session
|
213
|
-
start_console_session(workdir, include)
|
214
|
-
return
|
215
|
-
|
216
|
-
workdir = workdir or Path.cwd()
|
217
|
-
workdir = ensure_workdir(workdir)
|
218
|
-
|
219
|
-
if include:
|
220
|
-
include = [
|
221
|
-
path if path.is_absolute() else (workdir / path).resolve()
|
222
|
-
for path in include
|
223
|
-
]
|
224
|
-
|
225
36
|
if ask:
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
37
|
+
handle_ask(ask, workdir, include, False, agent)
|
38
|
+
elif play:
|
39
|
+
handle_play(play, workdir, False)
|
40
|
+
elif change_request == "scan":
|
230
41
|
paths_to_scan = include if include else [workdir]
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
if play:
|
235
|
-
replay_saved_file(play, claude, workdir, raw)
|
236
|
-
return
|
237
|
-
|
238
|
-
paths_to_scan = include if include else [workdir]
|
239
|
-
|
240
|
-
is_empty = is_dir_empty(workdir)
|
241
|
-
if is_empty and not include:
|
242
|
-
console = Console()
|
243
|
-
console.print("\n[bold blue]Empty directory - will create new files as needed[/bold blue]")
|
244
|
-
files_content = ""
|
42
|
+
handle_scan(paths_to_scan, workdir)
|
43
|
+
elif change_request:
|
44
|
+
handle_request(change_request, workdir, include, False, agent)
|
245
45
|
else:
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
initial_response = claude.send_message(initial_prompt)
|
250
|
-
analysis_file = save_to_file(initial_response, 'analysis', workdir)
|
251
|
-
|
252
|
-
format_analysis(initial_response, raw, claude)
|
253
|
-
|
254
|
-
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)
|
255
49
|
|
256
50
|
def main():
|
257
51
|
typer.run(typer_main)
|
258
52
|
|
259
53
|
if __name__ == "__main__":
|
260
|
-
main()
|
54
|
+
main()
|
janito/_contextparser.py
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
from typing import List, Tuple, Optional, NamedTuple
|
2
|
+
from difflib import SequenceMatcher
|
3
|
+
from janito.config import config
|
4
|
+
from rich.console import Console
|
5
|
+
|
6
|
+
class ContextError(NamedTuple):
|
7
|
+
"""Contains error details for context matching failures"""
|
8
|
+
pre_context: List[str]
|
9
|
+
post_context: List[str]
|
10
|
+
content: str
|
11
|
+
|
12
|
+
def parse_change_block(content: str) -> Tuple[List[str], List[str], List[str]]:
|
13
|
+
"""Parse a change block into pre-context, post-context and change lines.
|
14
|
+
Returns (pre_context_lines, post_context_lines, change_lines)"""
|
15
|
+
pre_context_lines = []
|
16
|
+
post_context_lines = []
|
17
|
+
change_lines = []
|
18
|
+
in_pre_context = True
|
19
|
+
|
20
|
+
for line in content.splitlines():
|
21
|
+
if line.startswith('='):
|
22
|
+
if in_pre_context:
|
23
|
+
pre_context_lines.append(line[1:])
|
24
|
+
else:
|
25
|
+
post_context_lines.append(line[1:])
|
26
|
+
elif line.startswith('>'):
|
27
|
+
in_pre_context = False
|
28
|
+
change_lines.append(line[1:])
|
29
|
+
|
30
|
+
return pre_context_lines, post_context_lines, change_lines
|
31
|
+
|
32
|
+
def find_context_match(file_content: str, pre_context: List[str], post_context: List[str], min_context: int = 2) -> Optional[Tuple[int, int]]:
|
33
|
+
"""Find exact matching location using line-by-line matching.
|
34
|
+
Returns (start_index, end_index) or None if no match found."""
|
35
|
+
if not (pre_context or post_context) or (len(pre_context) + len(post_context)) < min_context:
|
36
|
+
return None
|
37
|
+
|
38
|
+
file_lines = file_content.splitlines()
|
39
|
+
|
40
|
+
# Function to check if lines match at a given position
|
41
|
+
def lines_match_at(pos: int, target_lines: List[str]) -> bool:
|
42
|
+
if pos + len(target_lines) > len(file_lines):
|
43
|
+
return False
|
44
|
+
return all(a == b for a, b in zip(file_lines[pos:pos + len(target_lines)], target_lines))
|
45
|
+
|
46
|
+
# For debug output
|
47
|
+
debug_matches = []
|
48
|
+
|
49
|
+
# Try to find pre_context match
|
50
|
+
pre_match_pos = None
|
51
|
+
if pre_context:
|
52
|
+
for i in range(len(file_lines) - len(pre_context) + 1):
|
53
|
+
if lines_match_at(i, pre_context):
|
54
|
+
pre_match_pos = i
|
55
|
+
break
|
56
|
+
if config.debug:
|
57
|
+
# Record first 20 non-matches for debug output
|
58
|
+
if len(debug_matches) < 20:
|
59
|
+
debug_matches.append((i, file_lines[i:i + len(pre_context)]))
|
60
|
+
|
61
|
+
# Try to find post_context match after pre_context if found
|
62
|
+
if pre_match_pos is not None and post_context:
|
63
|
+
expected_post_pos = pre_match_pos + len(pre_context)
|
64
|
+
if not lines_match_at(expected_post_pos, post_context):
|
65
|
+
pre_match_pos = None
|
66
|
+
|
67
|
+
if pre_match_pos is None and config.debug:
|
68
|
+
console = Console()
|
69
|
+
console.print("\n[bold red]Context Match Debug:[/bold red]")
|
70
|
+
|
71
|
+
if pre_context:
|
72
|
+
console.print("\n[yellow]Expected pre-context:[/yellow]")
|
73
|
+
for i, line in enumerate(pre_context):
|
74
|
+
console.print(f" {i+1:2d} | '{line}'")
|
75
|
+
|
76
|
+
if post_context:
|
77
|
+
console.print("\n[yellow]Expected post-context:[/yellow]")
|
78
|
+
for i, line in enumerate(post_context):
|
79
|
+
console.print(f" {i+1:2d} | '{line}'")
|
80
|
+
|
81
|
+
console.print("\n[yellow]First 20 attempted matches in file:[/yellow]")
|
82
|
+
for pos, lines in debug_matches:
|
83
|
+
console.print(f"\n[cyan]At line {pos+1}:[/cyan]")
|
84
|
+
for i, line in enumerate(lines):
|
85
|
+
match_status = "≠" if i < len(pre_context) and line != pre_context[i] else "="
|
86
|
+
console.print(f" {i+1:2d} | '{line}' {match_status}")
|
87
|
+
|
88
|
+
return None
|
89
|
+
|
90
|
+
if pre_match_pos is None:
|
91
|
+
return None
|
92
|
+
|
93
|
+
end_pos = pre_match_pos + len(pre_context)
|
94
|
+
|
95
|
+
return pre_match_pos, end_pos
|
96
|
+
|
97
|
+
def apply_changes(content: str,
|
98
|
+
pre_context_lines: List[str],
|
99
|
+
post_context_lines: List[str],
|
100
|
+
change_lines: List[str]) -> Optional[Tuple[str, Optional[ContextError]]]:
|
101
|
+
"""Apply changes with context matching, returns (new_content, error_details)"""
|
102
|
+
if not content.strip() and not pre_context_lines and not post_context_lines:
|
103
|
+
return '\n'.join(change_lines), None
|
104
|
+
|
105
|
+
pre_context = '\n'.join(pre_context_lines)
|
106
|
+
post_context = '\n'.join(post_context_lines)
|
107
|
+
|
108
|
+
if pre_context and pre_context not in content:
|
109
|
+
return None, ContextError(pre_context_lines, post_context_lines, content)
|
110
|
+
|
111
|
+
if post_context and post_context not in content:
|
112
|
+
return None, ContextError(pre_context_lines, post_context_lines, content)
|
113
|
+
|
@@ -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
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import anthropic
|
2
|
+
import os
|
3
|
+
from typing import Optional
|
4
|
+
from threading import Event
|
5
|
+
from .agent import Agent
|
6
|
+
|
7
|
+
class ClaudeAIAgent(Agent):
|
8
|
+
"""Handles interaction with Claude API, including message handling"""
|
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)
|
14
|
+
if not system_prompt:
|
15
|
+
raise ValueError("system_prompt is required")
|
16
|
+
|
17
|
+
if not self.api_key:
|
18
|
+
raise ValueError("ANTHROPIC_API_KEY environment variable is required")
|
19
|
+
self.client = anthropic.Client(api_key=self.api_key)
|
20
|
+
self.model = os.getenv('CLAUDE_MODEL', self.DEFAULT_MODEL)
|
21
|
+
self.system_message = system_prompt
|
22
|
+
self.last_prompt = None
|
23
|
+
self.last_full_message = None
|
24
|
+
self.last_response = None
|
25
|
+
self.messages_history = []
|
26
|
+
if system_prompt:
|
27
|
+
self.messages_history.append(("system", system_prompt))
|
28
|
+
|
29
|
+
def send_message(self, message: str, stop_event: Event = None) -> str:
|
30
|
+
"""Send message to Claude API and return response"""
|
31
|
+
self.messages_history.append(("user", message))
|
32
|
+
# Store the full message
|
33
|
+
self.last_full_message = message
|
34
|
+
|
35
|
+
try:
|
36
|
+
# Check if already cancelled
|
37
|
+
if stop_event and stop_event.is_set():
|
38
|
+
return ""
|
39
|
+
|
40
|
+
response = self.client.messages.create(
|
41
|
+
model=self.model, # Use discovered model
|
42
|
+
system=self.system_message,
|
43
|
+
max_tokens=8192,
|
44
|
+
messages=[
|
45
|
+
{"role": "user", "content": message}
|
46
|
+
],
|
47
|
+
temperature=0,
|
48
|
+
)
|
49
|
+
|
50
|
+
# Handle response
|
51
|
+
response_text = response.content[0].text
|
52
|
+
|
53
|
+
# Only store and process response if not cancelled
|
54
|
+
if not (stop_event and stop_event.is_set()):
|
55
|
+
self.last_response = response_text
|
56
|
+
self.messages_history.append(("assistant", response_text))
|
57
|
+
|
58
|
+
# Always return the response, let caller handle cancellation
|
59
|
+
return response_text
|
60
|
+
|
61
|
+
except KeyboardInterrupt:
|
62
|
+
if stop_event:
|
63
|
+
stop_event.set()
|
64
|
+
return ""
|
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?")
|