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.
Files changed (51) hide show
  1. janito/__init__.py +48 -1
  2. janito/__main__.py +29 -235
  3. janito/_contextparser.py +113 -0
  4. janito/agents/__init__.py +22 -0
  5. janito/agents/agent.py +21 -0
  6. janito/agents/claudeai.py +64 -0
  7. janito/agents/openai.py +53 -0
  8. janito/agents/test.py +34 -0
  9. janito/analysis/__init__.py +33 -0
  10. janito/analysis/display.py +149 -0
  11. janito/analysis/options.py +112 -0
  12. janito/analysis/prompts.py +75 -0
  13. janito/change/__init__.py +19 -0
  14. janito/change/applier.py +269 -0
  15. janito/change/content.py +62 -0
  16. janito/change/indentation.py +33 -0
  17. janito/change/position.py +169 -0
  18. janito/changehistory.py +46 -0
  19. janito/changeviewer/__init__.py +12 -0
  20. janito/changeviewer/diff.py +28 -0
  21. janito/changeviewer/panels.py +268 -0
  22. janito/changeviewer/styling.py +59 -0
  23. janito/changeviewer/themes.py +57 -0
  24. janito/cli/__init__.py +2 -0
  25. janito/cli/commands.py +53 -0
  26. janito/cli/functions.py +286 -0
  27. janito/cli/registry.py +26 -0
  28. janito/common.py +23 -0
  29. janito/config.py +8 -3
  30. janito/console/__init__.py +3 -0
  31. janito/console/commands.py +112 -0
  32. janito/console/core.py +62 -0
  33. janito/console/display.py +157 -0
  34. janito/fileparser.py +334 -0
  35. janito/prompts.py +58 -74
  36. janito/qa.py +40 -7
  37. janito/review.py +13 -0
  38. janito/scan.py +68 -14
  39. janito/tests/test_fileparser.py +26 -0
  40. janito/version.py +23 -0
  41. janito-0.5.0.dist-info/METADATA +146 -0
  42. janito-0.5.0.dist-info/RECORD +45 -0
  43. janito/changeviewer.py +0 -64
  44. janito/claude.py +0 -74
  45. janito/console.py +0 -60
  46. janito/contentchange.py +0 -165
  47. janito-0.3.0.dist-info/METADATA +0 -138
  48. janito-0.3.0.dist-info/RECORD +0 -15
  49. {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/WHEEL +0 -0
  50. {janito-0.3.0.dist-info → janito-0.5.0.dist-info}/entry_points.txt +0 -0
  51. {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
- # Empty file to make the directory a Python package
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, Dict, Any, List
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 rich.markdown import Markdown
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
- def save_prompt_to_file(prompt: str) -> Path:
91
- """Save prompt to a named temporary file that won't be deleted"""
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
- answer = ask_question(question, files_content, claude)
165
- display_answer(answer, raw)
10
+ from .cli.commands import handle_request, handle_ask, handle_play, handle_scan
166
11
 
167
- def ensure_workdir(workdir: Path) -> Path:
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
- request: Optional[str] = typer.Argument(None, help="The modification request"),
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
- include: Optional[List[Path]] = typer.Option(None, "-i", "--include", help="Additional paths to include in analysis", exists=True),
189
- debug: bool = typer.Option(False, "--debug", help="Show debug information"),
190
- debug_line: Optional[int] = typer.Option(None, "--debug-line", help="Show debug information only for specific line number"),
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 v{get_version()}")
201
- raise typer.Exit()
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
- claude = ClaudeAPIAgent(system_prompt=SYSTEM_PROMPT)
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
- process_question(ask, workdir, include, raw, claude)
227
- return
228
-
229
- if scan:
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
- preview_scan(paths_to_scan, workdir)
232
- return
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
- files_content = collect_files_content(paths_to_scan, workdir)
247
-
248
- initial_prompt = build_request_analisys_prompt(files_content, request)
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()
@@ -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 ""
@@ -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?")