janito 0.13.0__py3-none-any.whl → 0.14.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- janito/__init__.py +1 -1
- janito/cli/agent/__init__.py +7 -0
- janito/cli/agent/conversation.py +149 -0
- janito/cli/agent/initialization.py +172 -0
- janito/cli/agent/query.py +108 -0
- janito/cli/agent.py +7 -395
- janito/cli/app.py +98 -10
- janito/cli/commands/__init__.py +12 -0
- janito/cli/commands/config.py +242 -0
- janito/cli/commands/history.py +119 -0
- janito/cli/commands/profile.py +72 -0
- janito/cli/commands/validation.py +24 -0
- janito/cli/commands/workspace.py +31 -0
- janito/cli/commands.py +9 -326
- janito/config.py +17 -0
- janito/data/instructions_template.txt +7 -4
- janito/tools/__init__.py +8 -2
- janito/tools/fetch_webpage/__init__.py +22 -33
- janito/tools/fetch_webpage/core.py +182 -155
- janito/tools/search_text.py +225 -239
- janito/tools/think.py +37 -0
- janito/tools/usage_tracker.py +1 -0
- {janito-0.13.0.dist-info → janito-0.14.0.dist-info}/METADATA +104 -8
- {janito-0.13.0.dist-info → janito-0.14.0.dist-info}/RECORD +28 -22
- janito/test_file.py +0 -4
- janito/tools/fetch_webpage/chunking.py +0 -76
- janito/tools/fetch_webpage/extractors.py +0 -276
- janito/tools/fetch_webpage/news.py +0 -137
- janito/tools/fetch_webpage/utils.py +0 -108
- {janito-0.13.0.dist-info → janito-0.14.0.dist-info}/WHEEL +0 -0
- {janito-0.13.0.dist-info → janito-0.14.0.dist-info}/entry_points.txt +0 -0
- {janito-0.13.0.dist-info → janito-0.14.0.dist-info}/licenses/LICENSE +0 -0
janito/__init__.py
CHANGED
@@ -0,0 +1,149 @@
|
|
1
|
+
"""
|
2
|
+
Conversation management functionality for Janito CLI.
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
import datetime
|
6
|
+
import sys
|
7
|
+
from typing import Optional, List, Dict, Any
|
8
|
+
from rich.console import Console
|
9
|
+
from pathlib import Path
|
10
|
+
import claudine
|
11
|
+
|
12
|
+
from janito.config import get_config
|
13
|
+
|
14
|
+
console = Console()
|
15
|
+
|
16
|
+
def generate_message_id() -> str:
|
17
|
+
"""
|
18
|
+
Generate a message ID based on timestamp with seconds granularity
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
str: A timestamp-based message ID
|
22
|
+
"""
|
23
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
24
|
+
return timestamp
|
25
|
+
|
26
|
+
def save_messages(agent: claudine.Agent) -> Optional[str]:
|
27
|
+
"""
|
28
|
+
Save agent messages to .janito/last_messages/{message_id}.json
|
29
|
+
|
30
|
+
Args:
|
31
|
+
agent: The claudine agent instance
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
str: The message ID used for saving, or None if saving failed
|
35
|
+
"""
|
36
|
+
try:
|
37
|
+
# Get the workspace directory
|
38
|
+
workspace_dir = Path(get_config().workspace_dir)
|
39
|
+
|
40
|
+
# Create .janito directory if it doesn't exist
|
41
|
+
janito_dir = workspace_dir / ".janito"
|
42
|
+
janito_dir.mkdir(exist_ok=True)
|
43
|
+
|
44
|
+
# Create last_messages directory if it doesn't exist
|
45
|
+
messages_dir = janito_dir / "last_messages"
|
46
|
+
messages_dir.mkdir(exist_ok=True)
|
47
|
+
|
48
|
+
# Generate a unique message ID
|
49
|
+
message_id = generate_message_id()
|
50
|
+
|
51
|
+
# Get messages from the agent
|
52
|
+
messages = agent.get_messages()
|
53
|
+
|
54
|
+
# Create a message object with metadata
|
55
|
+
message_object = {
|
56
|
+
"id": message_id,
|
57
|
+
"timestamp": datetime.datetime.now().isoformat(),
|
58
|
+
"messages": messages
|
59
|
+
}
|
60
|
+
|
61
|
+
# Save messages to file
|
62
|
+
message_file = messages_dir / f"{message_id}.json"
|
63
|
+
with open(message_file, "w", encoding="utf-8") as f:
|
64
|
+
json.dump(message_object, f, ensure_ascii=False, indent=2)
|
65
|
+
|
66
|
+
if get_config().verbose:
|
67
|
+
console.print(f"[bold green]✅ Conversation saved to {message_file}[/bold green]")
|
68
|
+
|
69
|
+
return message_id
|
70
|
+
except Exception as e:
|
71
|
+
console.print(f"[bold red]❌ Error saving conversation:[/bold red] {str(e)}")
|
72
|
+
return None
|
73
|
+
|
74
|
+
def load_messages(message_id: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
|
75
|
+
"""
|
76
|
+
Load messages from .janito/last_messages/{message_id}.json or the latest message file
|
77
|
+
|
78
|
+
Args:
|
79
|
+
message_id: Optional message ID to load specific conversation
|
80
|
+
|
81
|
+
Returns:
|
82
|
+
List of message dictionaries or None if file doesn't exist
|
83
|
+
"""
|
84
|
+
try:
|
85
|
+
# Get the workspace directory
|
86
|
+
workspace_dir = Path(get_config().workspace_dir)
|
87
|
+
janito_dir = workspace_dir / ".janito"
|
88
|
+
messages_dir = janito_dir / "last_messages"
|
89
|
+
|
90
|
+
# If message_id is provided, try to load that specific conversation
|
91
|
+
if message_id:
|
92
|
+
# Check if the message ID is a file name or just the ID
|
93
|
+
if message_id.endswith('.json'):
|
94
|
+
message_file = messages_dir / message_id
|
95
|
+
else:
|
96
|
+
message_file = messages_dir / f"{message_id}.json"
|
97
|
+
|
98
|
+
if not message_file.exists():
|
99
|
+
console.print(f"[bold yellow]⚠️ No conversation found with ID {message_id}[/bold yellow]")
|
100
|
+
return None
|
101
|
+
|
102
|
+
# Load messages from file
|
103
|
+
with open(message_file, "r", encoding="utf-8") as f:
|
104
|
+
message_object = json.load(f)
|
105
|
+
|
106
|
+
# Extract messages from the message object
|
107
|
+
if isinstance(message_object, dict) and "messages" in message_object:
|
108
|
+
messages = message_object["messages"]
|
109
|
+
else:
|
110
|
+
# Handle legacy format
|
111
|
+
messages = message_object
|
112
|
+
|
113
|
+
if get_config().verbose:
|
114
|
+
console.print(f"[bold green]✅ Loaded conversation from {message_file}[/bold green]")
|
115
|
+
console.print(f"[dim]📝 Conversation has {len(messages)} messages[/dim]")
|
116
|
+
|
117
|
+
return messages
|
118
|
+
|
119
|
+
# If no message_id is provided, try to load the latest message from last_messages directory
|
120
|
+
if not messages_dir.exists() or not any(messages_dir.iterdir()):
|
121
|
+
console.print("[bold yellow]⚠️ No previous conversation found[/bold yellow]")
|
122
|
+
return None
|
123
|
+
|
124
|
+
# Find the latest message file (based on filename which is a timestamp)
|
125
|
+
latest_file = max(
|
126
|
+
[f for f in messages_dir.iterdir() if f.is_file() and f.suffix == '.json'],
|
127
|
+
key=lambda x: x.stem
|
128
|
+
)
|
129
|
+
|
130
|
+
# Load messages from the latest file
|
131
|
+
with open(latest_file, "r", encoding="utf-8") as f:
|
132
|
+
message_object = json.load(f)
|
133
|
+
|
134
|
+
# Extract messages from the message object
|
135
|
+
if isinstance(message_object, dict) and "messages" in message_object:
|
136
|
+
messages = message_object["messages"]
|
137
|
+
else:
|
138
|
+
# Handle legacy format
|
139
|
+
messages = message_object
|
140
|
+
|
141
|
+
if get_config().verbose:
|
142
|
+
console.print(f"[bold green]✅ Loaded latest conversation from {latest_file}[/bold green]")
|
143
|
+
console.print(f"[dim]📝 Conversation has {len(messages)} messages[/dim]")
|
144
|
+
|
145
|
+
return messages
|
146
|
+
except Exception as e:
|
147
|
+
console.print(f"[bold red]❌ Error loading conversation:[/bold red] {str(e)}")
|
148
|
+
return None
|
149
|
+
|
@@ -0,0 +1,172 @@
|
|
1
|
+
"""
|
2
|
+
Agent initialization functionality for Janito CLI.
|
3
|
+
"""
|
4
|
+
import os
|
5
|
+
import platform
|
6
|
+
import typer
|
7
|
+
from typing import Optional, Dict, Any
|
8
|
+
from rich.console import Console
|
9
|
+
from jinja2 import Template
|
10
|
+
import importlib.resources as pkg_resources
|
11
|
+
import claudine
|
12
|
+
|
13
|
+
from janito.config import get_config, Config
|
14
|
+
from janito.callbacks import text_callback
|
15
|
+
from janito.tools import str_replace_editor, get_tools, reset_tracker
|
16
|
+
from janito.tools.bash.bash import bash_tool
|
17
|
+
from janito.cli.output import display_generation_params
|
18
|
+
|
19
|
+
console = Console()
|
20
|
+
|
21
|
+
def get_api_key() -> str:
|
22
|
+
"""
|
23
|
+
Get the API key from global config, environment variable, or user input.
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
str: The API key
|
27
|
+
"""
|
28
|
+
# Get API key from global config, environment variable, or ask the user
|
29
|
+
api_key = Config.get_api_key()
|
30
|
+
|
31
|
+
# If not found in global config, try environment variable
|
32
|
+
if not api_key:
|
33
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
34
|
+
|
35
|
+
# If still not found, prompt the user
|
36
|
+
if not api_key:
|
37
|
+
console.print("[bold yellow]⚠️ Warning:[/bold yellow] API key not found in global config or ANTHROPIC_API_KEY environment variable.")
|
38
|
+
console.print("🔑 Please set it using --set-api-key or provide your API key now:")
|
39
|
+
api_key = typer.prompt("Anthropic API Key", hide_input=True)
|
40
|
+
|
41
|
+
return api_key
|
42
|
+
|
43
|
+
def load_instructions() -> str:
|
44
|
+
"""
|
45
|
+
Load instructions template and render it with variables.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
str: The rendered instructions
|
49
|
+
"""
|
50
|
+
try:
|
51
|
+
# For Python 3.9+
|
52
|
+
try:
|
53
|
+
from importlib.resources import files
|
54
|
+
template_content = files('janito.data').joinpath('instructions_template.txt').read_text(encoding='utf-8')
|
55
|
+
# Fallback for older Python versions
|
56
|
+
except (ImportError, AttributeError):
|
57
|
+
template_content = pkg_resources.read_text('janito.data', 'instructions_template.txt', encoding='utf-8')
|
58
|
+
|
59
|
+
# Create template variables
|
60
|
+
template_variables = {
|
61
|
+
'platform': platform.system(),
|
62
|
+
'role': get_config().role,
|
63
|
+
# Add any other variables you want to pass to the template here
|
64
|
+
}
|
65
|
+
|
66
|
+
# Create template and render
|
67
|
+
template = Template(template_content)
|
68
|
+
instructions = template.render(**template_variables)
|
69
|
+
|
70
|
+
except Exception as e:
|
71
|
+
console.print(f"[bold red]❌ Error loading instructions template:[/bold red] {str(e)}")
|
72
|
+
# Try to fall back to regular instructions.txt
|
73
|
+
try:
|
74
|
+
# For Python 3.9+
|
75
|
+
try:
|
76
|
+
from importlib.resources import files
|
77
|
+
instructions = files('janito.data').joinpath('instructions.txt').read_text(encoding='utf-8')
|
78
|
+
# Fallback for older Python versions
|
79
|
+
except (ImportError, AttributeError):
|
80
|
+
instructions = pkg_resources.read_text('janito.data', 'instructions.txt', encoding='utf-8')
|
81
|
+
except Exception as e2:
|
82
|
+
console.print(f"[bold red]❌ Error loading fallback instructions:[/bold red] {str(e2)}")
|
83
|
+
instructions = "You are Janito, an AI assistant."
|
84
|
+
|
85
|
+
return instructions
|
86
|
+
|
87
|
+
def initialize_agent(temperature: float, verbose: bool, system_instructions: Optional[str] = None) -> claudine.Agent:
|
88
|
+
"""
|
89
|
+
Initialize the Claude agent with tools and configuration.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
temperature: Temperature value for model generation
|
93
|
+
verbose: Whether to enable verbose mode
|
94
|
+
system_instructions: Optional custom system instructions to use instead of loading from file
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
claudine.Agent: The initialized agent
|
98
|
+
"""
|
99
|
+
# Get API key
|
100
|
+
api_key = get_api_key()
|
101
|
+
|
102
|
+
# Load instructions or use provided system instructions
|
103
|
+
if system_instructions:
|
104
|
+
instructions = system_instructions
|
105
|
+
if verbose:
|
106
|
+
console.print("[bold blue]🔄 Using custom system instructions provided via --system parameter[/bold blue]")
|
107
|
+
# Print the first 50 characters of the instructions for verification
|
108
|
+
preview = system_instructions[:50] + "..." if len(system_instructions) > 50 else system_instructions
|
109
|
+
console.print(f"[dim]System instructions preview: {preview}[/dim]")
|
110
|
+
else:
|
111
|
+
instructions = load_instructions()
|
112
|
+
|
113
|
+
# Get tools
|
114
|
+
tools_list = get_tools()
|
115
|
+
|
116
|
+
# Reset usage tracker before each query
|
117
|
+
reset_tracker()
|
118
|
+
|
119
|
+
# Use command line parameters if provided (not default values), otherwise use config
|
120
|
+
temp_to_use = temperature if temperature != 0.0 else get_config().temperature
|
121
|
+
|
122
|
+
# Get profile parameters if a profile is set
|
123
|
+
config = get_config()
|
124
|
+
profile_data = None
|
125
|
+
if config.profile:
|
126
|
+
profile_data = config.get_available_profiles()[config.profile]
|
127
|
+
|
128
|
+
# Display generation parameters if verbose mode is enabled
|
129
|
+
if verbose:
|
130
|
+
display_generation_params(temp_to_use, profile_data, temperature)
|
131
|
+
|
132
|
+
# Create config_params dictionary with generation parameters
|
133
|
+
config_params = {
|
134
|
+
"temperature": temp_to_use
|
135
|
+
}
|
136
|
+
|
137
|
+
# Add top_k and top_p from profile if available
|
138
|
+
if profile_data:
|
139
|
+
if "top_k" in profile_data and profile_data["top_k"] != 0:
|
140
|
+
config_params["top_k"] = profile_data["top_k"]
|
141
|
+
if "top_p" in profile_data and profile_data["top_p"] != 0.0:
|
142
|
+
config_params["top_p"] = profile_data["top_p"]
|
143
|
+
|
144
|
+
# Initialize the agent
|
145
|
+
if get_config().no_tools:
|
146
|
+
# If no_tools mode is enabled, don't pass any tools to the agent
|
147
|
+
agent = claudine.Agent(
|
148
|
+
api_key=api_key,
|
149
|
+
system_prompt=instructions,
|
150
|
+
callbacks={"text": text_callback},
|
151
|
+
verbose=verbose,
|
152
|
+
max_tokens=8126,
|
153
|
+
max_tool_rounds=100,
|
154
|
+
config_params=config_params,
|
155
|
+
# Don't pass any tools, including text_editor_tool and bash_tool
|
156
|
+
)
|
157
|
+
else:
|
158
|
+
# Normal mode with tools
|
159
|
+
agent = claudine.Agent(
|
160
|
+
api_key=api_key,
|
161
|
+
system_prompt=instructions,
|
162
|
+
callbacks={"text": text_callback},
|
163
|
+
text_editor_tool=str_replace_editor,
|
164
|
+
bash_tool=bash_tool,
|
165
|
+
tools=tools_list,
|
166
|
+
verbose=verbose,
|
167
|
+
max_tokens=8126,
|
168
|
+
max_tool_rounds=100,
|
169
|
+
config_params=config_params,
|
170
|
+
)
|
171
|
+
|
172
|
+
return agent
|
@@ -0,0 +1,108 @@
|
|
1
|
+
"""
|
2
|
+
Query handling functionality for Janito CLI.
|
3
|
+
"""
|
4
|
+
import sys
|
5
|
+
import traceback
|
6
|
+
from typing import Optional
|
7
|
+
from rich.console import Console
|
8
|
+
import anthropic
|
9
|
+
|
10
|
+
from janito.token_report import generate_token_report
|
11
|
+
from janito.tools import print_usage_stats
|
12
|
+
from janito.cli.agent.initialization import initialize_agent
|
13
|
+
from janito.cli.agent.conversation import load_messages, save_messages
|
14
|
+
|
15
|
+
console = Console()
|
16
|
+
|
17
|
+
def handle_query(query: str, temperature: float, verbose: bool, show_tokens: bool, continue_conversation: Optional[str] = None, system_instructions: Optional[str] = None) -> None:
|
18
|
+
"""
|
19
|
+
Handle a query by initializing the agent and sending the query.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
query: The query to send to the agent
|
23
|
+
temperature: Temperature value for model generation
|
24
|
+
verbose: Whether to enable verbose mode
|
25
|
+
show_tokens: Whether to show detailed token usage
|
26
|
+
continue_conversation: Optional message ID to continue a specific conversation
|
27
|
+
system_instructions: Optional custom system instructions to use instead of loading from file
|
28
|
+
"""
|
29
|
+
# Initialize the agent
|
30
|
+
agent = initialize_agent(temperature, verbose, system_instructions)
|
31
|
+
|
32
|
+
# Load previous messages if continuing conversation
|
33
|
+
if continue_conversation is not None:
|
34
|
+
# If continue_conversation is an empty string (from flag with no value), use default behavior
|
35
|
+
message_id = None if continue_conversation == "" else continue_conversation
|
36
|
+
messages = load_messages(message_id)
|
37
|
+
if messages:
|
38
|
+
agent.set_messages(messages)
|
39
|
+
if message_id:
|
40
|
+
console.print(f"[bold blue]🔄 Continuing conversation with ID: {message_id}[/bold blue]")
|
41
|
+
else:
|
42
|
+
console.print("[bold blue]🔄 Continuing most recent conversation[/bold blue]")
|
43
|
+
|
44
|
+
# Provide information about the conversation being continued
|
45
|
+
if verbose and len(messages) > 0:
|
46
|
+
# Get the number of messages
|
47
|
+
num_messages = len(messages)
|
48
|
+
# Get the last user message if available
|
49
|
+
last_user_message = next((msg.get("content", "") for msg in reversed(messages)
|
50
|
+
if msg.get("role") == "user"), "")
|
51
|
+
if last_user_message:
|
52
|
+
console.print(f"[dim]📝 Last query: \"{last_user_message[:60]}{'...' if len(last_user_message) > 60 else ''}\"[/dim]")
|
53
|
+
else:
|
54
|
+
console.print("[bold yellow]⚠️ No previous conversation found to continue[/bold yellow]")
|
55
|
+
|
56
|
+
# Send the query to the agent
|
57
|
+
try:
|
58
|
+
agent.query(query)
|
59
|
+
|
60
|
+
# Save messages after successful query and get the message ID
|
61
|
+
message_id = save_messages(agent)
|
62
|
+
|
63
|
+
# Print token usage report
|
64
|
+
if show_tokens:
|
65
|
+
generate_token_report(agent, verbose=True, interrupted=False)
|
66
|
+
else:
|
67
|
+
# Show basic token usage
|
68
|
+
generate_token_report(agent, verbose=False, interrupted=False)
|
69
|
+
|
70
|
+
# Print tool usage statistics
|
71
|
+
print_usage_stats()
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
except KeyboardInterrupt:
|
76
|
+
# Handle Ctrl+C by printing token and tool usage information
|
77
|
+
console.print("\n[bold yellow]⚠️ Query interrupted by user (Ctrl+C)[/bold yellow]")
|
78
|
+
|
79
|
+
# Save messages even if interrupted
|
80
|
+
message_id = save_messages(agent)
|
81
|
+
|
82
|
+
# Print token usage report (even if interrupted)
|
83
|
+
try:
|
84
|
+
if show_tokens:
|
85
|
+
generate_token_report(agent, verbose=True, interrupted=True)
|
86
|
+
else:
|
87
|
+
# Show basic token usage
|
88
|
+
generate_token_report(agent, verbose=False, interrupted=True)
|
89
|
+
|
90
|
+
# Print tool usage statistics
|
91
|
+
print_usage_stats()
|
92
|
+
|
93
|
+
|
94
|
+
except Exception as e:
|
95
|
+
console.print(f"[bold red]❌ Error generating usage report:[/bold red] {str(e)}")
|
96
|
+
if verbose:
|
97
|
+
console.print(traceback.format_exc())
|
98
|
+
|
99
|
+
# Exit with non-zero status to indicate interruption
|
100
|
+
sys.exit(130) # 130 is the standard exit code for SIGINT
|
101
|
+
|
102
|
+
except anthropic.APIError as e:
|
103
|
+
console.print(f"[bold red]❌ Anthropic API Error:[/bold red] {str(e)}")
|
104
|
+
|
105
|
+
except Exception as e:
|
106
|
+
console.print(f"[bold red]❌ Error:[/bold red] {str(e)}")
|
107
|
+
if verbose:
|
108
|
+
console.print(traceback.format_exc())
|