janito 0.15.0__tar.gz → 1.0.0__tar.gz
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-0.15.0 → janito-1.0.0}/LICENSE +2 -2
- janito-1.0.0/PKG-INFO +144 -0
- janito-1.0.0/README.md +123 -0
- janito-1.0.0/janito/__init__.py +1 -0
- janito-1.0.0/janito/__main__.py +5 -0
- janito-1.0.0/janito/agent/__init__.py +1 -0
- janito-1.0.0/janito/agent/agent.py +96 -0
- janito-1.0.0/janito/agent/config.py +113 -0
- janito-1.0.0/janito/agent/config_defaults.py +10 -0
- janito-1.0.0/janito/agent/conversation.py +107 -0
- janito-1.0.0/janito/agent/queued_tool_handler.py +16 -0
- janito-1.0.0/janito/agent/runtime_config.py +30 -0
- janito-1.0.0/janito/agent/tool_handler.py +124 -0
- janito-1.0.0/janito/agent/tools/__init__.py +11 -0
- janito-1.0.0/janito/agent/tools/ask_user.py +63 -0
- janito-1.0.0/janito/agent/tools/bash_exec.py +58 -0
- janito-1.0.0/janito/agent/tools/create_directory.py +19 -0
- janito-1.0.0/janito/agent/tools/create_file.py +43 -0
- janito-1.0.0/janito/agent/tools/fetch_url.py +48 -0
- janito-1.0.0/janito/agent/tools/file_str_replace.py +48 -0
- janito-1.0.0/janito/agent/tools/find_files.py +37 -0
- janito-1.0.0/janito/agent/tools/gitignore_utils.py +40 -0
- janito-1.0.0/janito/agent/tools/move_file.py +37 -0
- janito-1.0.0/janito/agent/tools/remove_file.py +19 -0
- janito-1.0.0/janito/agent/tools/rich_live.py +37 -0
- janito-1.0.0/janito/agent/tools/rich_utils.py +31 -0
- janito-1.0.0/janito/agent/tools/search_text.py +41 -0
- janito-1.0.0/janito/agent/tools/view_file.py +34 -0
- janito-1.0.0/janito/cli/__init__.py +0 -0
- janito-1.0.0/janito/cli/_print_config.py +68 -0
- janito-1.0.0/janito/cli/_utils.py +8 -0
- janito-1.0.0/janito/cli/arg_parser.py +26 -0
- janito-1.0.0/janito/cli/config_commands.py +131 -0
- janito-1.0.0/janito/cli/logging_setup.py +27 -0
- janito-1.0.0/janito/cli/main.py +39 -0
- janito-1.0.0/janito/cli/runner.py +135 -0
- janito-1.0.0/janito/cli_chat_shell/__init__.py +1 -0
- janito-1.0.0/janito/cli_chat_shell/chat_loop.py +147 -0
- janito-1.0.0/janito/cli_chat_shell/commands.py +202 -0
- janito-1.0.0/janito/cli_chat_shell/config_shell.py +75 -0
- janito-1.0.0/janito/cli_chat_shell/load_prompt.py +15 -0
- janito-1.0.0/janito/cli_chat_shell/session_manager.py +60 -0
- janito-1.0.0/janito/cli_chat_shell/ui.py +136 -0
- janito-1.0.0/janito/render_prompt.py +12 -0
- janito-1.0.0/janito/templates/system_instructions.j2 +36 -0
- janito-1.0.0/janito/web/__init__.py +0 -0
- janito-1.0.0/janito/web/__main__.py +17 -0
- janito-1.0.0/janito/web/app.py +132 -0
- janito-1.0.0/janito.egg-info/PKG-INFO +144 -0
- janito-1.0.0/janito.egg-info/SOURCES.txt +54 -0
- janito-1.0.0/janito.egg-info/dependency_links.txt +1 -0
- janito-1.0.0/janito.egg-info/entry_points.txt +2 -0
- janito-1.0.0/janito.egg-info/requires.txt +4 -0
- janito-1.0.0/janito.egg-info/top_level.txt +1 -0
- janito-1.0.0/pyproject.toml +36 -0
- janito-1.0.0/setup.cfg +4 -0
- janito-0.15.0/.gitignore +0 -114
- janito-0.15.0/PKG-INFO +0 -481
- janito-0.15.0/README.md +0 -461
- janito-0.15.0/janito/__init__.py +0 -5
- janito-0.15.0/janito/__main__.py +0 -7
- janito-0.15.0/janito/callbacks.py +0 -34
- janito-0.15.0/janito/cli/__init__.py +0 -6
- janito-0.15.0/janito/cli/agent/__init__.py +0 -7
- janito-0.15.0/janito/cli/agent/conversation.py +0 -149
- janito-0.15.0/janito/cli/agent/initialization.py +0 -168
- janito-0.15.0/janito/cli/agent/query.py +0 -112
- janito-0.15.0/janito/cli/agent.py +0 -12
- janito-0.15.0/janito/cli/app.py +0 -178
- janito-0.15.0/janito/cli/commands/__init__.py +0 -12
- janito-0.15.0/janito/cli/commands/config.py +0 -30
- janito-0.15.0/janito/cli/commands/history.py +0 -119
- janito-0.15.0/janito/cli/commands/profile.py +0 -93
- janito-0.15.0/janito/cli/commands/validation.py +0 -24
- janito-0.15.0/janito/cli/commands/workspace.py +0 -31
- janito-0.15.0/janito/cli/commands.py +0 -12
- janito-0.15.0/janito/cli/output.py +0 -29
- janito-0.15.0/janito/cli/utils.py +0 -22
- janito-0.15.0/janito/config/README.md +0 -104
- janito-0.15.0/janito/config/__init__.py +0 -16
- janito-0.15.0/janito/config/cli/__init__.py +0 -28
- janito-0.15.0/janito/config/cli/commands.py +0 -397
- janito-0.15.0/janito/config/cli/validators.py +0 -77
- janito-0.15.0/janito/config/core/__init__.py +0 -23
- janito-0.15.0/janito/config/core/file_operations.py +0 -90
- janito-0.15.0/janito/config/core/properties.py +0 -316
- janito-0.15.0/janito/config/core/singleton.py +0 -282
- janito-0.15.0/janito/config/profiles/__init__.py +0 -8
- janito-0.15.0/janito/config/profiles/definitions.py +0 -38
- janito-0.15.0/janito/config/profiles/manager.py +0 -80
- janito-0.15.0/janito/data/instructions_template.txt +0 -34
- janito-0.15.0/janito/token_report.py +0 -154
- janito-0.15.0/janito/tools/__init__.py +0 -44
- janito-0.15.0/janito/tools/bash/bash.py +0 -157
- janito-0.15.0/janito/tools/bash/unix_persistent_bash.py +0 -215
- janito-0.15.0/janito/tools/bash/win_persistent_bash.py +0 -341
- janito-0.15.0/janito/tools/decorators.py +0 -90
- janito-0.15.0/janito/tools/delete_file.py +0 -65
- janito-0.15.0/janito/tools/fetch_webpage/__init__.py +0 -23
- janito-0.15.0/janito/tools/fetch_webpage/core.py +0 -182
- janito-0.15.0/janito/tools/find_files.py +0 -220
- janito-0.15.0/janito/tools/move_file.py +0 -72
- janito-0.15.0/janito/tools/prompt_user.py +0 -57
- janito-0.15.0/janito/tools/replace_file.py +0 -63
- janito-0.15.0/janito/tools/rich_console.py +0 -176
- janito-0.15.0/janito/tools/search_text.py +0 -226
- janito-0.15.0/janito/tools/str_replace_editor/__init__.py +0 -6
- janito-0.15.0/janito/tools/str_replace_editor/editor.py +0 -55
- janito-0.15.0/janito/tools/str_replace_editor/handlers/__init__.py +0 -16
- janito-0.15.0/janito/tools/str_replace_editor/handlers/create.py +0 -60
- janito-0.15.0/janito/tools/str_replace_editor/handlers/insert.py +0 -100
- janito-0.15.0/janito/tools/str_replace_editor/handlers/str_replace.py +0 -94
- janito-0.15.0/janito/tools/str_replace_editor/handlers/undo.py +0 -64
- janito-0.15.0/janito/tools/str_replace_editor/handlers/view.py +0 -165
- janito-0.15.0/janito/tools/str_replace_editor/utils.py +0 -33
- janito-0.15.0/janito/tools/think.py +0 -37
- janito-0.15.0/janito/tools/usage_tracker.py +0 -137
- janito-0.15.0/pyproject.toml +0 -89
@@ -1,6 +1,6 @@
|
|
1
1
|
MIT License
|
2
2
|
|
3
|
-
Copyright (c) [year] [
|
3
|
+
Copyright (c) [year] [Full Name]
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
SOFTWARE.
|
21
|
+
SOFTWARE.
|
janito-1.0.0/PKG-INFO
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: janito
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: An agent framework with built-in tools.
|
5
|
+
Author-email: João Pinto <joao.pinto@gmail.com>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: homepage, https://github.com/joaompinto/janito
|
8
|
+
Project-URL: repository, https://github.com/joaompinto/janito
|
9
|
+
Keywords: agent,framework,tools,automation
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Requires-Python: >=3.8
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
License-File: LICENSE
|
16
|
+
Requires-Dist: rich
|
17
|
+
Requires-Dist: openai
|
18
|
+
Requires-Dist: flask
|
19
|
+
Requires-Dist: pathspec
|
20
|
+
Dynamic: license-file
|
21
|
+
|
22
|
+
# 🚀 Janito: Natural Language Code Editing Agent
|
23
|
+
|
24
|
+
## ⚡ Quick Start
|
25
|
+
|
26
|
+
Run a one-off prompt:
|
27
|
+
```bash
|
28
|
+
python -m janito "Refactor the data processing module to improve readability."
|
29
|
+
```
|
30
|
+
|
31
|
+
Or start the interactive chat shell:
|
32
|
+
```bash
|
33
|
+
python -m janito
|
34
|
+
```
|
35
|
+
|
36
|
+
Launch the web UI:
|
37
|
+
```bash
|
38
|
+
python -m janito.web
|
39
|
+
```
|
40
|
+
|
41
|
+
---
|
42
|
+
|
43
|
+
Janito is a command-line and web-based AI agent designed to **edit code and manage files** using natural language instructions.
|
44
|
+
|
45
|
+
---
|
46
|
+
|
47
|
+
## ✨ Key Features
|
48
|
+
- 📝 **Code Editing via Natural Language:** Modify, create, or delete code files simply by describing the changes.
|
49
|
+
- 📁 **File & Directory Management:** Navigate, create, move, or remove files and folders.
|
50
|
+
- 🧠 **Context-Aware:** Understands your project structure for precise edits.
|
51
|
+
- 💬 **Interactive User Prompts:** Asks for clarification when needed.
|
52
|
+
- 🧩 **Extensible Tooling:** Built-in tools for file operations, shell commands, and more.
|
53
|
+
- 🌐 **Web Interface (In Development):** Upcoming simple web UI for streaming responses and tool progress.
|
54
|
+
|
55
|
+
---
|
56
|
+
|
57
|
+
## 📦 Installation
|
58
|
+
|
59
|
+
### Requirements
|
60
|
+
- Python 3.8+
|
61
|
+
|
62
|
+
### Install dependencies
|
63
|
+
```bash
|
64
|
+
pip install -e .
|
65
|
+
```
|
66
|
+
|
67
|
+
### Set your API key
|
68
|
+
Janito uses OpenAI-compatible APIs (default: `openrouter/optimus-alpha`). Set your API key using the CLI:
|
69
|
+
```bash
|
70
|
+
python -m janito --set-api-key your_api_key_here
|
71
|
+
```
|
72
|
+
|
73
|
+
### Obtain an API key from openrouter.io
|
74
|
+
1. Visit [https://openrouter.io/](https://openrouter.io/)
|
75
|
+
2. Sign in or create a free account.
|
76
|
+
3. Navigate to **API Keys** in your account dashboard.
|
77
|
+
4. Click **Create new key**, provide a name, and save the generated key.
|
78
|
+
5. Save it using the CLI:
|
79
|
+
```bash
|
80
|
+
python -m janito --set-api-key your_api_key_here
|
81
|
+
```
|
82
|
+
|
83
|
+
---
|
84
|
+
|
85
|
+
## ⚙️ Configuration
|
86
|
+
|
87
|
+
Janito supports multiple ways to configure API access, model, and behavior:
|
88
|
+
|
89
|
+
### API Key
|
90
|
+
|
91
|
+
- Set via CLI:
|
92
|
+
```bash
|
93
|
+
python -m janito --set-api-key your_api_key_here
|
94
|
+
```
|
95
|
+
|
96
|
+
### Configurable Options
|
97
|
+
|
98
|
+
| Key | Description | How to set | Default |
|
99
|
+
|-----------------|-----------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------|
|
100
|
+
| `api_key` | API key for OpenAI-compatible service | `--set-api-key`, config file | _None_ (required) |
|
101
|
+
| `model` | Model name to use | `--set-local-config model=...` or `--set-global-config` | `openrouter/optimus-alpha` |
|
102
|
+
| `base_url` | API base URL (OpenAI-compatible endpoint) | `--set-local-config base_url=...` or `--set-global-config` | `https://openrouter.ai/api/v1` |
|
103
|
+
| `role` | Role description for system prompt | CLI `--role` or config | "software engineer" |
|
104
|
+
| `system_prompt` | Override the entire system prompt | CLI `--system-prompt` or config | _Template-generated prompt_ |
|
105
|
+
| `temperature` | Sampling temperature (float, e.g., 0.0 - 2.0) | CLI `--temperature` or config | 0.2 |
|
106
|
+
| `max_tokens` | Maximum tokens for model response | CLI `--max-tokens` or config | 200000 |
|
107
|
+
|
108
|
+
### Config files
|
109
|
+
|
110
|
+
- **Local config:** `.janito/config.json` (project-specific)
|
111
|
+
- **Global config:** `~/.config/janito/config.json` (user-wide)
|
112
|
+
|
113
|
+
Set values via:
|
114
|
+
|
115
|
+
```bash
|
116
|
+
python -m janito --set-local-config key=value
|
117
|
+
python -m janito --set-global-config key=value
|
118
|
+
```
|
119
|
+
|
120
|
+
---
|
121
|
+
|
122
|
+
## 🚀 Build and Release
|
123
|
+
|
124
|
+
Janito provides scripts for automated build and release to PyPI:
|
125
|
+
|
126
|
+
### Bash (Linux/macOS)
|
127
|
+
|
128
|
+
```bash
|
129
|
+
./tools/release.sh
|
130
|
+
```
|
131
|
+
|
132
|
+
### PowerShell (Windows)
|
133
|
+
|
134
|
+
```powershell
|
135
|
+
./tools/release.ps1
|
136
|
+
```
|
137
|
+
|
138
|
+
These scripts will:
|
139
|
+
- Check for required tools (`hatch`, `twine`)
|
140
|
+
- Validate the version in `pyproject.toml` against PyPI and git tags
|
141
|
+
- Build the package
|
142
|
+
- Upload to PyPI
|
143
|
+
|
144
|
+
---
|
janito-1.0.0/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# 🚀 Janito: Natural Language Code Editing Agent
|
2
|
+
|
3
|
+
## ⚡ Quick Start
|
4
|
+
|
5
|
+
Run a one-off prompt:
|
6
|
+
```bash
|
7
|
+
python -m janito "Refactor the data processing module to improve readability."
|
8
|
+
```
|
9
|
+
|
10
|
+
Or start the interactive chat shell:
|
11
|
+
```bash
|
12
|
+
python -m janito
|
13
|
+
```
|
14
|
+
|
15
|
+
Launch the web UI:
|
16
|
+
```bash
|
17
|
+
python -m janito.web
|
18
|
+
```
|
19
|
+
|
20
|
+
---
|
21
|
+
|
22
|
+
Janito is a command-line and web-based AI agent designed to **edit code and manage files** using natural language instructions.
|
23
|
+
|
24
|
+
---
|
25
|
+
|
26
|
+
## ✨ Key Features
|
27
|
+
- 📝 **Code Editing via Natural Language:** Modify, create, or delete code files simply by describing the changes.
|
28
|
+
- 📁 **File & Directory Management:** Navigate, create, move, or remove files and folders.
|
29
|
+
- 🧠 **Context-Aware:** Understands your project structure for precise edits.
|
30
|
+
- 💬 **Interactive User Prompts:** Asks for clarification when needed.
|
31
|
+
- 🧩 **Extensible Tooling:** Built-in tools for file operations, shell commands, and more.
|
32
|
+
- 🌐 **Web Interface (In Development):** Upcoming simple web UI for streaming responses and tool progress.
|
33
|
+
|
34
|
+
---
|
35
|
+
|
36
|
+
## 📦 Installation
|
37
|
+
|
38
|
+
### Requirements
|
39
|
+
- Python 3.8+
|
40
|
+
|
41
|
+
### Install dependencies
|
42
|
+
```bash
|
43
|
+
pip install -e .
|
44
|
+
```
|
45
|
+
|
46
|
+
### Set your API key
|
47
|
+
Janito uses OpenAI-compatible APIs (default: `openrouter/optimus-alpha`). Set your API key using the CLI:
|
48
|
+
```bash
|
49
|
+
python -m janito --set-api-key your_api_key_here
|
50
|
+
```
|
51
|
+
|
52
|
+
### Obtain an API key from openrouter.io
|
53
|
+
1. Visit [https://openrouter.io/](https://openrouter.io/)
|
54
|
+
2. Sign in or create a free account.
|
55
|
+
3. Navigate to **API Keys** in your account dashboard.
|
56
|
+
4. Click **Create new key**, provide a name, and save the generated key.
|
57
|
+
5. Save it using the CLI:
|
58
|
+
```bash
|
59
|
+
python -m janito --set-api-key your_api_key_here
|
60
|
+
```
|
61
|
+
|
62
|
+
---
|
63
|
+
|
64
|
+
## ⚙️ Configuration
|
65
|
+
|
66
|
+
Janito supports multiple ways to configure API access, model, and behavior:
|
67
|
+
|
68
|
+
### API Key
|
69
|
+
|
70
|
+
- Set via CLI:
|
71
|
+
```bash
|
72
|
+
python -m janito --set-api-key your_api_key_here
|
73
|
+
```
|
74
|
+
|
75
|
+
### Configurable Options
|
76
|
+
|
77
|
+
| Key | Description | How to set | Default |
|
78
|
+
|-----------------|-----------------------------------------------------------|-----------------------------------------------------------------|--------------------------------------------|
|
79
|
+
| `api_key` | API key for OpenAI-compatible service | `--set-api-key`, config file | _None_ (required) |
|
80
|
+
| `model` | Model name to use | `--set-local-config model=...` or `--set-global-config` | `openrouter/optimus-alpha` |
|
81
|
+
| `base_url` | API base URL (OpenAI-compatible endpoint) | `--set-local-config base_url=...` or `--set-global-config` | `https://openrouter.ai/api/v1` |
|
82
|
+
| `role` | Role description for system prompt | CLI `--role` or config | "software engineer" |
|
83
|
+
| `system_prompt` | Override the entire system prompt | CLI `--system-prompt` or config | _Template-generated prompt_ |
|
84
|
+
| `temperature` | Sampling temperature (float, e.g., 0.0 - 2.0) | CLI `--temperature` or config | 0.2 |
|
85
|
+
| `max_tokens` | Maximum tokens for model response | CLI `--max-tokens` or config | 200000 |
|
86
|
+
|
87
|
+
### Config files
|
88
|
+
|
89
|
+
- **Local config:** `.janito/config.json` (project-specific)
|
90
|
+
- **Global config:** `~/.config/janito/config.json` (user-wide)
|
91
|
+
|
92
|
+
Set values via:
|
93
|
+
|
94
|
+
```bash
|
95
|
+
python -m janito --set-local-config key=value
|
96
|
+
python -m janito --set-global-config key=value
|
97
|
+
```
|
98
|
+
|
99
|
+
---
|
100
|
+
|
101
|
+
## 🚀 Build and Release
|
102
|
+
|
103
|
+
Janito provides scripts for automated build and release to PyPI:
|
104
|
+
|
105
|
+
### Bash (Linux/macOS)
|
106
|
+
|
107
|
+
```bash
|
108
|
+
./tools/release.sh
|
109
|
+
```
|
110
|
+
|
111
|
+
### PowerShell (Windows)
|
112
|
+
|
113
|
+
```powershell
|
114
|
+
./tools/release.ps1
|
115
|
+
```
|
116
|
+
|
117
|
+
These scripts will:
|
118
|
+
- Check for required tools (`hatch`, `twine`)
|
119
|
+
- Validate the version in `pyproject.toml` against PyPI and git tags
|
120
|
+
- Build the package
|
121
|
+
- Upload to PyPI
|
122
|
+
|
123
|
+
---
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "1.0.0"
|
@@ -0,0 +1 @@
|
|
1
|
+
from . import tools
|
@@ -0,0 +1,96 @@
|
|
1
|
+
"""Agent module: defines the core LLM agent with tool and conversation handling."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import json
|
5
|
+
from openai import OpenAI
|
6
|
+
from janito.agent.conversation import ConversationHandler
|
7
|
+
from janito.agent.tool_handler import ToolHandler
|
8
|
+
|
9
|
+
class Agent:
|
10
|
+
"""LLM Agent capable of handling conversations and tool calls."""
|
11
|
+
|
12
|
+
REFERER = "www.janito.dev"
|
13
|
+
TITLE = "Janito"
|
14
|
+
|
15
|
+
def __init__(
|
16
|
+
self,
|
17
|
+
api_key: str,
|
18
|
+
model: str = None,
|
19
|
+
system_prompt: str | None = None,
|
20
|
+
verbose_tools: bool = False,
|
21
|
+
tool_handler = None,
|
22
|
+
base_url: str = "https://openrouter.ai/api/v1"
|
23
|
+
):
|
24
|
+
"""
|
25
|
+
Initialize the Agent.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
api_key: API key for OpenAI-compatible service.
|
29
|
+
model: Model name to use.
|
30
|
+
system_prompt: Optional system prompt override.
|
31
|
+
verbose_tools: Enable verbose tool call logging.
|
32
|
+
tool_handler: Optional custom ToolHandler instance.
|
33
|
+
base_url: API base URL.
|
34
|
+
"""
|
35
|
+
self.api_key = api_key
|
36
|
+
self.model = model
|
37
|
+
self.system_prompt = system_prompt
|
38
|
+
if os.environ.get("USE_AZURE_OPENAI"):
|
39
|
+
from openai import AzureOpenAI
|
40
|
+
self.client = AzureOpenAI(
|
41
|
+
api_key=api_key,
|
42
|
+
azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
|
43
|
+
api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2023-05-15"),
|
44
|
+
)
|
45
|
+
else:
|
46
|
+
self.client = OpenAI(
|
47
|
+
base_url=base_url,
|
48
|
+
api_key=api_key,
|
49
|
+
default_headers={
|
50
|
+
"HTTP-Referer": self.REFERER,
|
51
|
+
"X-Title": self.TITLE
|
52
|
+
}
|
53
|
+
)
|
54
|
+
if tool_handler is not None:
|
55
|
+
self.tool_handler = tool_handler
|
56
|
+
else:
|
57
|
+
self.tool_handler = ToolHandler(verbose=verbose_tools)
|
58
|
+
|
59
|
+
self.conversation_handler = ConversationHandler(
|
60
|
+
self.client, self.model, self.tool_handler
|
61
|
+
)
|
62
|
+
|
63
|
+
def chat(self, messages, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None):
|
64
|
+
import time
|
65
|
+
from janito.agent.conversation import ProviderError
|
66
|
+
|
67
|
+
max_retries = 5
|
68
|
+
for attempt in range(1, max_retries + 1):
|
69
|
+
try:
|
70
|
+
return self.conversation_handler.handle_conversation(
|
71
|
+
messages,
|
72
|
+
max_tokens=max_tokens,
|
73
|
+
on_content=on_content,
|
74
|
+
on_tool_progress=on_tool_progress,
|
75
|
+
verbose_response=verbose_response,
|
76
|
+
spinner=spinner
|
77
|
+
)
|
78
|
+
except ProviderError as e:
|
79
|
+
error_data = getattr(e, 'error_data', {}) or {}
|
80
|
+
code = error_data.get('code', '')
|
81
|
+
# Retry only on 5xx errors
|
82
|
+
if isinstance(code, int) and 500 <= code < 600:
|
83
|
+
pass
|
84
|
+
elif isinstance(code, str) and code.isdigit() and 500 <= int(code) < 600:
|
85
|
+
code = int(code)
|
86
|
+
else:
|
87
|
+
raise
|
88
|
+
|
89
|
+
if attempt < max_retries:
|
90
|
+
print(f"ProviderError with 5xx code encountered (attempt {attempt}/{max_retries}). Retrying in 5 seconds...")
|
91
|
+
time.sleep(5)
|
92
|
+
else:
|
93
|
+
print("Max retries reached. Raising error.")
|
94
|
+
raise
|
95
|
+
except Exception:
|
96
|
+
raise
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
from pathlib import Path
|
4
|
+
from threading import Lock
|
5
|
+
|
6
|
+
|
7
|
+
class SingletonMeta(type):
|
8
|
+
_instances = {}
|
9
|
+
_lock: Lock = Lock()
|
10
|
+
|
11
|
+
def __call__(cls, *args, **kwargs):
|
12
|
+
with cls._lock:
|
13
|
+
if cls not in cls._instances:
|
14
|
+
instance = super().__call__(*args, **kwargs)
|
15
|
+
cls._instances[cls] = instance
|
16
|
+
return cls._instances[cls]
|
17
|
+
|
18
|
+
|
19
|
+
class BaseConfig:
|
20
|
+
def __init__(self):
|
21
|
+
self._data = {}
|
22
|
+
|
23
|
+
def get(self, key, default=None):
|
24
|
+
return self._data.get(key, default)
|
25
|
+
|
26
|
+
def set(self, key, value):
|
27
|
+
self._data[key] = value
|
28
|
+
|
29
|
+
def all(self):
|
30
|
+
return self._data
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
class FileConfig(BaseConfig):
|
36
|
+
def __init__(self, path):
|
37
|
+
super().__init__()
|
38
|
+
self.path = Path(path).expanduser()
|
39
|
+
self.load()
|
40
|
+
|
41
|
+
def load(self):
|
42
|
+
if self.path.exists():
|
43
|
+
try:
|
44
|
+
with open(self.path, 'r') as f:
|
45
|
+
self._data = json.load(f)
|
46
|
+
# Remove keys with value None (null in JSON)
|
47
|
+
self._data = {k: v for k, v in self._data.items() if v is not None}
|
48
|
+
except Exception as e:
|
49
|
+
print(f"Warning: Failed to load config file {self.path}: {e}")
|
50
|
+
self._data = {}
|
51
|
+
else:
|
52
|
+
self._data = {}
|
53
|
+
|
54
|
+
def save(self):
|
55
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
56
|
+
with open(self.path, 'w') as f:
|
57
|
+
json.dump(self._data, f, indent=2)
|
58
|
+
|
59
|
+
|
60
|
+
CONFIG_OPTIONS = {
|
61
|
+
"api_key": "API key for OpenAI-compatible service (required)",
|
62
|
+
"model": "Model name to use (e.g., 'openrouter/optimus-alpha')",
|
63
|
+
"base_url": "API base URL (OpenAI-compatible endpoint)",
|
64
|
+
"role": "Role description for the system prompt (e.g., 'software engineer')",
|
65
|
+
"system_prompt": "Override the entire system prompt text",
|
66
|
+
"temperature": "Sampling temperature (float, e.g., 0.0 - 2.0)",
|
67
|
+
"max_tokens": "Maximum tokens for model response (int)"
|
68
|
+
}
|
69
|
+
|
70
|
+
# Import defaults for reference
|
71
|
+
from .config_defaults import CONFIG_DEFAULTS
|
72
|
+
|
73
|
+
class EffectiveConfig:
|
74
|
+
"""Read-only merged view of local and global configs"""
|
75
|
+
def __init__(self, local_cfg, global_cfg):
|
76
|
+
self.local_cfg = local_cfg
|
77
|
+
self.global_cfg = global_cfg
|
78
|
+
|
79
|
+
def get(self, key, default=None):
|
80
|
+
from .config_defaults import CONFIG_DEFAULTS
|
81
|
+
for cfg in (self.local_cfg, self.global_cfg):
|
82
|
+
val = cfg.get(key)
|
83
|
+
if val is not None:
|
84
|
+
# Treat explicit None/null as not set
|
85
|
+
if val is None:
|
86
|
+
continue
|
87
|
+
return val
|
88
|
+
# Use centralized defaults if no config found
|
89
|
+
if default is None and key in CONFIG_DEFAULTS:
|
90
|
+
return CONFIG_DEFAULTS[key]
|
91
|
+
return default
|
92
|
+
|
93
|
+
def all(self):
|
94
|
+
merged = {}
|
95
|
+
# Start with global, override with local
|
96
|
+
for cfg in (self.global_cfg, self.local_cfg):
|
97
|
+
merged.update(cfg.all())
|
98
|
+
return merged
|
99
|
+
|
100
|
+
|
101
|
+
# Singleton instances
|
102
|
+
|
103
|
+
local_config = FileConfig(Path('.janito/config.json'))
|
104
|
+
global_config = FileConfig(Path.home() / '.janito/config.json')
|
105
|
+
|
106
|
+
effective_config = EffectiveConfig(local_config, global_config)
|
107
|
+
|
108
|
+
def get_api_key():
|
109
|
+
"""Retrieve API key from config files (local, then global)."""
|
110
|
+
api_key = effective_config.get("api_key")
|
111
|
+
if api_key:
|
112
|
+
return api_key
|
113
|
+
raise ValueError("API key not found. Please configure 'api_key' in your config.")
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# Centralized config defaults for Janito
|
2
|
+
CONFIG_DEFAULTS = {
|
3
|
+
"api_key": None, # Must be set by user
|
4
|
+
"model": "openrouter/optimus-alpha", # Default model
|
5
|
+
"base_url": "https://openrouter.ai/api/v1",
|
6
|
+
"role": "software engineer",
|
7
|
+
"system_prompt": None, # None means auto-generate from role
|
8
|
+
"temperature": 0.2,
|
9
|
+
"max_tokens": 200000,
|
10
|
+
}
|
@@ -0,0 +1,107 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
class MaxRoundsExceededError(Exception):
|
4
|
+
pass
|
5
|
+
|
6
|
+
class EmptyResponseError(Exception):
|
7
|
+
pass
|
8
|
+
|
9
|
+
class ProviderError(Exception):
|
10
|
+
def __init__(self, message, error_data):
|
11
|
+
self.error_data = error_data
|
12
|
+
super().__init__(message)
|
13
|
+
|
14
|
+
class ConversationHandler:
|
15
|
+
def __init__(self, client, model, tool_handler):
|
16
|
+
self.client = client
|
17
|
+
self.model = model
|
18
|
+
self.tool_handler = tool_handler
|
19
|
+
|
20
|
+
def handle_conversation(self, messages, max_rounds=50, on_content=None, on_tool_progress=None, verbose_response=False, spinner=False, max_tokens=None):
|
21
|
+
if not messages:
|
22
|
+
raise ValueError("No prompt provided in messages")
|
23
|
+
|
24
|
+
from rich.console import Console
|
25
|
+
console = Console()
|
26
|
+
|
27
|
+
from janito.agent.runtime_config import unified_config
|
28
|
+
|
29
|
+
# Resolve max_tokens priority: runtime param > config > default
|
30
|
+
resolved_max_tokens = max_tokens
|
31
|
+
if resolved_max_tokens is None:
|
32
|
+
resolved_max_tokens = unified_config.get('max_tokens', 200000)
|
33
|
+
|
34
|
+
for _ in range(max_rounds):
|
35
|
+
if spinner:
|
36
|
+
# Calculate word count for all messages
|
37
|
+
word_count = sum(len(str(m.get('content', '')).split()) for m in messages if 'content' in m)
|
38
|
+
spinner_msg = f"[bold green]Waiting for AI response... ({word_count} words in conversation)"
|
39
|
+
with console.status(spinner_msg, spinner="dots") as status:
|
40
|
+
response = self.client.chat.completions.create(
|
41
|
+
model=self.model,
|
42
|
+
messages=messages,
|
43
|
+
tools=self.tool_handler.get_tool_schemas(),
|
44
|
+
tool_choice="auto",
|
45
|
+
temperature=0.2,
|
46
|
+
max_tokens=resolved_max_tokens
|
47
|
+
)
|
48
|
+
status.stop()
|
49
|
+
# console.print("\r\033[2K", end="") # Clear the spinner line removed
|
50
|
+
else:
|
51
|
+
response = self.client.chat.completions.create(
|
52
|
+
model=self.model,
|
53
|
+
messages=messages,
|
54
|
+
tools=self.tool_handler.get_tool_schemas(),
|
55
|
+
tool_choice="auto",
|
56
|
+
temperature=0.2,
|
57
|
+
max_tokens=resolved_max_tokens
|
58
|
+
)
|
59
|
+
|
60
|
+
if verbose_response:
|
61
|
+
import pprint
|
62
|
+
pprint.pprint(response)
|
63
|
+
|
64
|
+
# Check for provider errors
|
65
|
+
if hasattr(response, 'error') and response.error:
|
66
|
+
error_msg = response.error.get('message', 'Unknown provider error')
|
67
|
+
error_code = response.error.get('code', 'unknown')
|
68
|
+
raise ProviderError(f"Provider error: {error_msg} (Code: {error_code})", response.error)
|
69
|
+
|
70
|
+
if not response.choices:
|
71
|
+
raise EmptyResponseError("The LLM API returned no choices in the response.")
|
72
|
+
|
73
|
+
choice = response.choices[0]
|
74
|
+
|
75
|
+
# Extract token usage info if available
|
76
|
+
usage = getattr(response, 'usage', None)
|
77
|
+
if usage:
|
78
|
+
usage_info = {
|
79
|
+
'prompt_tokens': getattr(usage, 'prompt_tokens', None),
|
80
|
+
'completion_tokens': getattr(usage, 'completion_tokens', None),
|
81
|
+
'total_tokens': getattr(usage, 'total_tokens', None)
|
82
|
+
}
|
83
|
+
else:
|
84
|
+
usage_info = None
|
85
|
+
|
86
|
+
# Call the on_content callback if provided and content is not None
|
87
|
+
if on_content is not None and choice.message.content is not None:
|
88
|
+
on_content({"content": choice.message.content})
|
89
|
+
|
90
|
+
# If no tool calls, return the assistant's message and usage info
|
91
|
+
if not choice.message.tool_calls:
|
92
|
+
return {
|
93
|
+
"content": choice.message.content,
|
94
|
+
"usage": usage_info
|
95
|
+
}
|
96
|
+
|
97
|
+
tool_responses = []
|
98
|
+
for tool_call in choice.message.tool_calls:
|
99
|
+
result = self.tool_handler.handle_tool_call(tool_call, on_progress=on_tool_progress)
|
100
|
+
tool_responses.append({"tool_call_id": tool_call.id, "content": result})
|
101
|
+
|
102
|
+
messages.append({"role": "assistant", "content": choice.message.content, "tool_calls": [tc.to_dict() for tc in choice.message.tool_calls]})
|
103
|
+
|
104
|
+
for tr in tool_responses:
|
105
|
+
messages.append({"role": "tool", "tool_call_id": tr["tool_call_id"], "content": tr["content"]})
|
106
|
+
|
107
|
+
raise MaxRoundsExceededError("Max conversation rounds exceeded")
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from janito.agent.tool_handler import ToolHandler
|
2
|
+
|
3
|
+
class QueuedToolHandler(ToolHandler):
|
4
|
+
def __init__(self, queue, *args, **kwargs):
|
5
|
+
super().__init__(*args, **kwargs)
|
6
|
+
self._queue = queue
|
7
|
+
|
8
|
+
def handle_tool_call(self, tool_call, on_progress=None):
|
9
|
+
def enqueue_progress(data):
|
10
|
+
|
11
|
+
self._queue.put(('tool_progress', data))
|
12
|
+
|
13
|
+
if on_progress is None:
|
14
|
+
on_progress = enqueue_progress
|
15
|
+
|
16
|
+
return super().handle_tool_call(tool_call, on_progress=on_progress)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from .config import BaseConfig, EffectiveConfig, effective_config
|
2
|
+
|
3
|
+
class RuntimeConfig(BaseConfig):
|
4
|
+
"""In-memory only config, reset on restart"""
|
5
|
+
pass
|
6
|
+
|
7
|
+
runtime_config = RuntimeConfig()
|
8
|
+
|
9
|
+
class UnifiedConfig:
|
10
|
+
"""
|
11
|
+
Config lookup order:
|
12
|
+
1. runtime_config (in-memory, highest priority)
|
13
|
+
2. effective_config (local/global, read-only)
|
14
|
+
"""
|
15
|
+
def __init__(self, runtime_cfg, effective_cfg):
|
16
|
+
self.runtime_cfg = runtime_cfg
|
17
|
+
self.effective_cfg = effective_cfg
|
18
|
+
|
19
|
+
def get(self, key, default=None):
|
20
|
+
val = self.runtime_cfg.get(key)
|
21
|
+
if val is not None:
|
22
|
+
return val
|
23
|
+
return self.effective_cfg.get(key, default)
|
24
|
+
|
25
|
+
def all(self):
|
26
|
+
merged = dict(self.effective_cfg.all())
|
27
|
+
merged.update(self.runtime_cfg.all())
|
28
|
+
return merged
|
29
|
+
|
30
|
+
unified_config = UnifiedConfig(runtime_config, effective_config)
|