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.
Files changed (104) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +102 -326
  3. janito/agents/__init__.py +16 -0
  4. janito/agents/agent.py +21 -0
  5. janito/{claude.py → agents/claudeai.py} +13 -17
  6. janito/agents/openai.py +53 -0
  7. janito/agents/test.py +34 -0
  8. janito/change/__init__.py +32 -0
  9. janito/change/__main__.py +0 -0
  10. janito/change/analysis/__init__.py +23 -0
  11. janito/change/analysis/__main__.py +7 -0
  12. janito/change/analysis/analyze.py +61 -0
  13. janito/change/analysis/formatting.py +78 -0
  14. janito/change/analysis/options.py +81 -0
  15. janito/change/analysis/prompts.py +98 -0
  16. janito/change/analysis/view/__init__.py +9 -0
  17. janito/change/analysis/view/terminal.py +171 -0
  18. janito/change/applier/__init__.py +5 -0
  19. janito/change/applier/file.py +58 -0
  20. janito/change/applier/main.py +156 -0
  21. janito/change/applier/text.py +245 -0
  22. janito/change/applier/workspace_dir.py +58 -0
  23. janito/change/core.py +131 -0
  24. janito/change/history.py +44 -0
  25. janito/change/operations.py +7 -0
  26. janito/change/parser.py +289 -0
  27. janito/change/play.py +54 -0
  28. janito/change/preview.py +82 -0
  29. janito/change/prompts.py +126 -0
  30. janito/change/test.py +0 -0
  31. janito/change/validator.py +251 -0
  32. janito/change/viewer/__init__.py +11 -0
  33. janito/change/viewer/content.py +66 -0
  34. janito/change/viewer/diff.py +43 -0
  35. janito/change/viewer/pager.py +56 -0
  36. janito/change/viewer/panels.py +555 -0
  37. janito/change/viewer/styling.py +103 -0
  38. janito/change/viewer/themes.py +55 -0
  39. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  40. janito/clear_statement_parser/examples.txt +326 -0
  41. janito/clear_statement_parser/models.py +104 -0
  42. janito/clear_statement_parser/parser.py +496 -0
  43. janito/cli/__init__.py +2 -0
  44. janito/cli/base.py +30 -0
  45. janito/cli/commands.py +45 -0
  46. janito/cli/functions.py +111 -0
  47. janito/cli/handlers/ask.py +22 -0
  48. janito/cli/handlers/demo.py +22 -0
  49. janito/cli/handlers/request.py +24 -0
  50. janito/cli/handlers/scan.py +9 -0
  51. janito/cli/history.py +61 -0
  52. janito/cli/registry.py +26 -0
  53. janito/common.py +41 -10
  54. janito/config.py +71 -6
  55. janito/demo/__init__.py +4 -0
  56. janito/demo/data.py +13 -0
  57. janito/demo/mock_data.py +20 -0
  58. janito/demo/operations.py +45 -0
  59. janito/demo/runner.py +59 -0
  60. janito/demo/scenarios.py +32 -0
  61. janito/prompts.py +1 -65
  62. janito/qa.py +8 -5
  63. janito/review.py +13 -0
  64. janito/search_replace/README.md +146 -0
  65. janito/search_replace/__init__.py +6 -0
  66. janito/search_replace/__main__.py +21 -0
  67. janito/search_replace/core.py +119 -0
  68. janito/search_replace/parser.py +52 -0
  69. janito/search_replace/play.py +61 -0
  70. janito/search_replace/replacer.py +36 -0
  71. janito/search_replace/searcher.py +299 -0
  72. janito/shell/__init__.py +39 -0
  73. janito/shell/bus.py +31 -0
  74. janito/shell/commands.py +195 -0
  75. janito/shell/handlers.py +122 -0
  76. janito/shell/history.py +20 -0
  77. janito/shell/processor.py +52 -0
  78. janito/tui/__init__.py +21 -0
  79. janito/tui/base.py +22 -0
  80. janito/tui/flows/__init__.py +5 -0
  81. janito/tui/flows/changes.py +65 -0
  82. janito/tui/flows/content.py +128 -0
  83. janito/tui/flows/selection.py +117 -0
  84. janito/tui/screens/__init__.py +3 -0
  85. janito/tui/screens/app.py +1 -0
  86. janito/workspace/__init__.py +7 -0
  87. janito/workspace/analysis.py +121 -0
  88. janito/workspace/manager.py +48 -0
  89. janito/workspace/scan.py +232 -0
  90. janito-0.6.0.dist-info/METADATA +185 -0
  91. janito-0.6.0.dist-info/RECORD +95 -0
  92. {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
  93. janito/analysis.py +0 -281
  94. janito/changeapplier.py +0 -436
  95. janito/changeviewer.py +0 -350
  96. janito/console.py +0 -330
  97. janito/contentchange.py +0 -84
  98. janito/contextparser.py +0 -113
  99. janito/fileparser.py +0 -125
  100. janito/scan.py +0 -137
  101. janito-0.4.0.dist-info/METADATA +0 -164
  102. janito-0.4.0.dist-info/RECORD +0 -21
  103. {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
  104. {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
janito/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
+ """Core package initialization for Janito."""
1
2
 
2
- # Empty file to make the directory a Python package
janito/__main__.py CHANGED
@@ -1,356 +1,132 @@
1
- import sys
2
1
  import typer
3
- from typing import Optional, Dict, Any, List
2
+ from typing import Optional, List, Set
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
- )
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 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']}]")
8
+ from .version import get_version
61
9
 
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')
10
+ from janito.agents import agent
11
+ from janito.config import config
85
12
 
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
13
+ from .cli.commands import handle_request, handle_ask, handle_play, handle_scan
92
14
 
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
15
+ app = typer.Typer(add_completion=False)
101
16
 
102
- def modify_request(request: str) -> str:
103
- """Display current request and get modified version with improved formatting"""
104
- console = Console()
17
+ def validate_paths(paths: Optional[List[Path]]) -> Optional[List[Path]]:
18
+ """Validate include paths for duplicates.
105
19
 
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()
20
+ Args:
21
+ paths: List of paths to validate, or None if no paths provided
150
22
 
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()
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
- 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)
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
- def ensure_workdir(workdir: Path) -> Path:
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
- request: Optional[str] = typer.Argument(None, help="The modification request"),
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 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
-
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 v{get_version()}")
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
- 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
- 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 scan:
329
- paths_to_scan = include if include else [workdir]
330
- preview_scan(paths_to_scan, workdir)
74
+ if history:
75
+ from janito.cli.history import display_history
76
+ display_history()
331
77
  return
332
78
 
333
- if play:
334
- replay_saved_file(play, claude, workdir, raw)
335
- return
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
- 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 = ""
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
- files_content = collect_files_content(paths_to_scan, workdir)
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 ClaudeAPIAgent:
7
+ class ClaudeAIAgent(Agent):
7
8
  """Handles interaction with Claude API, including message handling"""
8
- def __init__(self, api_key: Optional[str] = None, system_prompt: str = None):
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
- self.api_key = api_key or os.getenv('ANTHROPIC_API_KEY')
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 = "claude-3-5-sonnet-20241022"
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
- self.messages_history = []
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=4000,
41
+ max_tokens=8192,
39
42
  messages=[
40
43
  {"role": "user", "content": message}
41
44
  ],
42
45
  temperature=0,
43
46
  )
44
47
 
45
- # Handle response
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 response_text
50
+ return response
55
51
 
56
52
  except KeyboardInterrupt:
57
53
  if stop_event:
@@ -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?")