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.
Files changed (118) hide show
  1. {janito-0.15.0 → janito-1.0.0}/LICENSE +2 -2
  2. janito-1.0.0/PKG-INFO +144 -0
  3. janito-1.0.0/README.md +123 -0
  4. janito-1.0.0/janito/__init__.py +1 -0
  5. janito-1.0.0/janito/__main__.py +5 -0
  6. janito-1.0.0/janito/agent/__init__.py +1 -0
  7. janito-1.0.0/janito/agent/agent.py +96 -0
  8. janito-1.0.0/janito/agent/config.py +113 -0
  9. janito-1.0.0/janito/agent/config_defaults.py +10 -0
  10. janito-1.0.0/janito/agent/conversation.py +107 -0
  11. janito-1.0.0/janito/agent/queued_tool_handler.py +16 -0
  12. janito-1.0.0/janito/agent/runtime_config.py +30 -0
  13. janito-1.0.0/janito/agent/tool_handler.py +124 -0
  14. janito-1.0.0/janito/agent/tools/__init__.py +11 -0
  15. janito-1.0.0/janito/agent/tools/ask_user.py +63 -0
  16. janito-1.0.0/janito/agent/tools/bash_exec.py +58 -0
  17. janito-1.0.0/janito/agent/tools/create_directory.py +19 -0
  18. janito-1.0.0/janito/agent/tools/create_file.py +43 -0
  19. janito-1.0.0/janito/agent/tools/fetch_url.py +48 -0
  20. janito-1.0.0/janito/agent/tools/file_str_replace.py +48 -0
  21. janito-1.0.0/janito/agent/tools/find_files.py +37 -0
  22. janito-1.0.0/janito/agent/tools/gitignore_utils.py +40 -0
  23. janito-1.0.0/janito/agent/tools/move_file.py +37 -0
  24. janito-1.0.0/janito/agent/tools/remove_file.py +19 -0
  25. janito-1.0.0/janito/agent/tools/rich_live.py +37 -0
  26. janito-1.0.0/janito/agent/tools/rich_utils.py +31 -0
  27. janito-1.0.0/janito/agent/tools/search_text.py +41 -0
  28. janito-1.0.0/janito/agent/tools/view_file.py +34 -0
  29. janito-1.0.0/janito/cli/__init__.py +0 -0
  30. janito-1.0.0/janito/cli/_print_config.py +68 -0
  31. janito-1.0.0/janito/cli/_utils.py +8 -0
  32. janito-1.0.0/janito/cli/arg_parser.py +26 -0
  33. janito-1.0.0/janito/cli/config_commands.py +131 -0
  34. janito-1.0.0/janito/cli/logging_setup.py +27 -0
  35. janito-1.0.0/janito/cli/main.py +39 -0
  36. janito-1.0.0/janito/cli/runner.py +135 -0
  37. janito-1.0.0/janito/cli_chat_shell/__init__.py +1 -0
  38. janito-1.0.0/janito/cli_chat_shell/chat_loop.py +147 -0
  39. janito-1.0.0/janito/cli_chat_shell/commands.py +202 -0
  40. janito-1.0.0/janito/cli_chat_shell/config_shell.py +75 -0
  41. janito-1.0.0/janito/cli_chat_shell/load_prompt.py +15 -0
  42. janito-1.0.0/janito/cli_chat_shell/session_manager.py +60 -0
  43. janito-1.0.0/janito/cli_chat_shell/ui.py +136 -0
  44. janito-1.0.0/janito/render_prompt.py +12 -0
  45. janito-1.0.0/janito/templates/system_instructions.j2 +36 -0
  46. janito-1.0.0/janito/web/__init__.py +0 -0
  47. janito-1.0.0/janito/web/__main__.py +17 -0
  48. janito-1.0.0/janito/web/app.py +132 -0
  49. janito-1.0.0/janito.egg-info/PKG-INFO +144 -0
  50. janito-1.0.0/janito.egg-info/SOURCES.txt +54 -0
  51. janito-1.0.0/janito.egg-info/dependency_links.txt +1 -0
  52. janito-1.0.0/janito.egg-info/entry_points.txt +2 -0
  53. janito-1.0.0/janito.egg-info/requires.txt +4 -0
  54. janito-1.0.0/janito.egg-info/top_level.txt +1 -0
  55. janito-1.0.0/pyproject.toml +36 -0
  56. janito-1.0.0/setup.cfg +4 -0
  57. janito-0.15.0/.gitignore +0 -114
  58. janito-0.15.0/PKG-INFO +0 -481
  59. janito-0.15.0/README.md +0 -461
  60. janito-0.15.0/janito/__init__.py +0 -5
  61. janito-0.15.0/janito/__main__.py +0 -7
  62. janito-0.15.0/janito/callbacks.py +0 -34
  63. janito-0.15.0/janito/cli/__init__.py +0 -6
  64. janito-0.15.0/janito/cli/agent/__init__.py +0 -7
  65. janito-0.15.0/janito/cli/agent/conversation.py +0 -149
  66. janito-0.15.0/janito/cli/agent/initialization.py +0 -168
  67. janito-0.15.0/janito/cli/agent/query.py +0 -112
  68. janito-0.15.0/janito/cli/agent.py +0 -12
  69. janito-0.15.0/janito/cli/app.py +0 -178
  70. janito-0.15.0/janito/cli/commands/__init__.py +0 -12
  71. janito-0.15.0/janito/cli/commands/config.py +0 -30
  72. janito-0.15.0/janito/cli/commands/history.py +0 -119
  73. janito-0.15.0/janito/cli/commands/profile.py +0 -93
  74. janito-0.15.0/janito/cli/commands/validation.py +0 -24
  75. janito-0.15.0/janito/cli/commands/workspace.py +0 -31
  76. janito-0.15.0/janito/cli/commands.py +0 -12
  77. janito-0.15.0/janito/cli/output.py +0 -29
  78. janito-0.15.0/janito/cli/utils.py +0 -22
  79. janito-0.15.0/janito/config/README.md +0 -104
  80. janito-0.15.0/janito/config/__init__.py +0 -16
  81. janito-0.15.0/janito/config/cli/__init__.py +0 -28
  82. janito-0.15.0/janito/config/cli/commands.py +0 -397
  83. janito-0.15.0/janito/config/cli/validators.py +0 -77
  84. janito-0.15.0/janito/config/core/__init__.py +0 -23
  85. janito-0.15.0/janito/config/core/file_operations.py +0 -90
  86. janito-0.15.0/janito/config/core/properties.py +0 -316
  87. janito-0.15.0/janito/config/core/singleton.py +0 -282
  88. janito-0.15.0/janito/config/profiles/__init__.py +0 -8
  89. janito-0.15.0/janito/config/profiles/definitions.py +0 -38
  90. janito-0.15.0/janito/config/profiles/manager.py +0 -80
  91. janito-0.15.0/janito/data/instructions_template.txt +0 -34
  92. janito-0.15.0/janito/token_report.py +0 -154
  93. janito-0.15.0/janito/tools/__init__.py +0 -44
  94. janito-0.15.0/janito/tools/bash/bash.py +0 -157
  95. janito-0.15.0/janito/tools/bash/unix_persistent_bash.py +0 -215
  96. janito-0.15.0/janito/tools/bash/win_persistent_bash.py +0 -341
  97. janito-0.15.0/janito/tools/decorators.py +0 -90
  98. janito-0.15.0/janito/tools/delete_file.py +0 -65
  99. janito-0.15.0/janito/tools/fetch_webpage/__init__.py +0 -23
  100. janito-0.15.0/janito/tools/fetch_webpage/core.py +0 -182
  101. janito-0.15.0/janito/tools/find_files.py +0 -220
  102. janito-0.15.0/janito/tools/move_file.py +0 -72
  103. janito-0.15.0/janito/tools/prompt_user.py +0 -57
  104. janito-0.15.0/janito/tools/replace_file.py +0 -63
  105. janito-0.15.0/janito/tools/rich_console.py +0 -176
  106. janito-0.15.0/janito/tools/search_text.py +0 -226
  107. janito-0.15.0/janito/tools/str_replace_editor/__init__.py +0 -6
  108. janito-0.15.0/janito/tools/str_replace_editor/editor.py +0 -55
  109. janito-0.15.0/janito/tools/str_replace_editor/handlers/__init__.py +0 -16
  110. janito-0.15.0/janito/tools/str_replace_editor/handlers/create.py +0 -60
  111. janito-0.15.0/janito/tools/str_replace_editor/handlers/insert.py +0 -100
  112. janito-0.15.0/janito/tools/str_replace_editor/handlers/str_replace.py +0 -94
  113. janito-0.15.0/janito/tools/str_replace_editor/handlers/undo.py +0 -64
  114. janito-0.15.0/janito/tools/str_replace_editor/handlers/view.py +0 -165
  115. janito-0.15.0/janito/tools/str_replace_editor/utils.py +0 -33
  116. janito-0.15.0/janito/tools/think.py +0 -37
  117. janito-0.15.0/janito/tools/usage_tracker.py +0 -137
  118. janito-0.15.0/pyproject.toml +0 -89
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) [year] [copyright holder]
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,5 @@
1
+ from janito.cli.main import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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)