kubrick-cli 0.1.2__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.
- kubrick_cli/__init__.py +47 -0
- kubrick_cli/agent_loop.py +307 -0
- kubrick_cli/classifier.py +198 -0
- kubrick_cli/config.py +248 -0
- kubrick_cli/display.py +165 -0
- kubrick_cli/execution_strategy.py +197 -0
- kubrick_cli/main.py +797 -0
- kubrick_cli/planning.py +328 -0
- kubrick_cli/progress.py +172 -0
- kubrick_cli/providers/__init__.py +6 -0
- kubrick_cli/providers/anthropic_provider.py +224 -0
- kubrick_cli/providers/base.py +137 -0
- kubrick_cli/providers/factory.py +183 -0
- kubrick_cli/providers/openai_provider.py +190 -0
- kubrick_cli/providers/triton_provider.py +96 -0
- kubrick_cli/safety.py +216 -0
- kubrick_cli/scheduler.py +197 -0
- kubrick_cli/setup_wizard.py +172 -0
- kubrick_cli/tools.py +407 -0
- kubrick_cli/triton_client.py +192 -0
- kubrick_cli-0.1.2.dist-info/METADATA +86 -0
- kubrick_cli-0.1.2.dist-info/RECORD +26 -0
- kubrick_cli-0.1.2.dist-info/WHEEL +5 -0
- kubrick_cli-0.1.2.dist-info/entry_points.txt +2 -0
- kubrick_cli-0.1.2.dist-info/licenses/LICENSE +21 -0
- kubrick_cli-0.1.2.dist-info/top_level.txt +1 -0
kubrick_cli/main.py
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Main CLI entry point for Kubrick."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.markdown import Markdown
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.prompt import Prompt
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
from .agent_loop import AgentLoop
|
|
19
|
+
from .classifier import TaskClassifier
|
|
20
|
+
from .config import KubrickConfig
|
|
21
|
+
from .display import DisplayManager
|
|
22
|
+
from .execution_strategy import ExecutionStrategy
|
|
23
|
+
from .planning import PlanningPhase
|
|
24
|
+
from .providers.factory import ProviderFactory
|
|
25
|
+
from .safety import SafetyConfig, SafetyManager
|
|
26
|
+
from .scheduler import ToolScheduler
|
|
27
|
+
from .tools import TOOL_DEFINITIONS, ToolExecutor, get_tools_prompt
|
|
28
|
+
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class KubrickCLI:
|
|
33
|
+
"""Main CLI application."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
config: KubrickConfig,
|
|
38
|
+
working_dir: str = None,
|
|
39
|
+
conversation_id: str = None,
|
|
40
|
+
provider_override: str = None,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize Kubrick CLI.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config: KubrickConfig instance
|
|
47
|
+
working_dir: Working directory for file operations (overrides config)
|
|
48
|
+
conversation_id: Load existing conversation by ID
|
|
49
|
+
provider_override: Override configured provider (for testing)
|
|
50
|
+
"""
|
|
51
|
+
self.config = config
|
|
52
|
+
|
|
53
|
+
# Override provider if specified
|
|
54
|
+
if provider_override:
|
|
55
|
+
config.set("provider", provider_override)
|
|
56
|
+
|
|
57
|
+
# Create provider using factory
|
|
58
|
+
try:
|
|
59
|
+
self.provider = ProviderFactory.create_provider(config.get_all())
|
|
60
|
+
console.print(
|
|
61
|
+
f"[dim]→ Using {self.provider.provider_name} provider "
|
|
62
|
+
f"with model {self.provider.model_name}[/dim]"
|
|
63
|
+
)
|
|
64
|
+
except ValueError as e:
|
|
65
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
66
|
+
console.print(
|
|
67
|
+
"[yellow]Please run the setup wizard or check your configuration.[/yellow]"
|
|
68
|
+
)
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
# Alias for backward compatibility
|
|
72
|
+
self.client = self.provider
|
|
73
|
+
|
|
74
|
+
# Initialize safety manager
|
|
75
|
+
self.safety_manager = SafetyManager(
|
|
76
|
+
SafetyConfig.from_config(config.get_all())
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Initialize tool executor with safety manager
|
|
80
|
+
self.tool_executor = ToolExecutor(
|
|
81
|
+
working_dir=working_dir, safety_manager=self.safety_manager
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Initialize display manager
|
|
85
|
+
self.display_manager = DisplayManager(config.get_all())
|
|
86
|
+
|
|
87
|
+
# Initialize tool scheduler
|
|
88
|
+
enable_parallel = config.get("enable_parallel_tools", True)
|
|
89
|
+
max_workers = config.get("max_parallel_workers", 3)
|
|
90
|
+
self.tool_scheduler = ToolScheduler(
|
|
91
|
+
tool_executor=self.tool_executor,
|
|
92
|
+
max_workers=max_workers,
|
|
93
|
+
enable_parallel=enable_parallel,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Initialize agent loop with config values
|
|
97
|
+
max_iterations = config.get("max_iterations", 15)
|
|
98
|
+
max_tools_per_turn = config.get("max_tools_per_turn", 5)
|
|
99
|
+
timeout_seconds = config.get("total_timeout_seconds", 600)
|
|
100
|
+
|
|
101
|
+
self.agent_loop = AgentLoop(
|
|
102
|
+
llm_client=self.provider,
|
|
103
|
+
tool_executor=self.tool_executor,
|
|
104
|
+
max_iterations=max_iterations,
|
|
105
|
+
max_tools_per_turn=max_tools_per_turn,
|
|
106
|
+
timeout_seconds=timeout_seconds,
|
|
107
|
+
stream_options={},
|
|
108
|
+
display_manager=self.display_manager,
|
|
109
|
+
tool_scheduler=self.tool_scheduler,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Initialize task classifier and planning phase
|
|
113
|
+
self.classifier = TaskClassifier(self.provider)
|
|
114
|
+
self.planning_phase = PlanningPhase(
|
|
115
|
+
llm_client=self.provider,
|
|
116
|
+
tool_executor=self.tool_executor,
|
|
117
|
+
agent_loop=self.agent_loop,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Interrupt handling state
|
|
121
|
+
self.interrupt_count = 0
|
|
122
|
+
|
|
123
|
+
# Generate conversation ID
|
|
124
|
+
self.conversation_id = conversation_id or datetime.now().strftime(
|
|
125
|
+
"%Y%m%d_%H%M%S"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Initialize or load conversation
|
|
129
|
+
if conversation_id:
|
|
130
|
+
loaded = self._load_conversation(conversation_id)
|
|
131
|
+
if loaded:
|
|
132
|
+
self.messages = loaded
|
|
133
|
+
else:
|
|
134
|
+
console.print(
|
|
135
|
+
f"[yellow]Conversation {conversation_id} not found, starting new conversation[/yellow]"
|
|
136
|
+
)
|
|
137
|
+
self.messages = self._get_initial_messages()
|
|
138
|
+
else:
|
|
139
|
+
self.messages = self._get_initial_messages()
|
|
140
|
+
|
|
141
|
+
def _get_initial_messages(self) -> list:
|
|
142
|
+
"""Get initial system prompt messages."""
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
"role": "system",
|
|
146
|
+
"content": f"""You are Kubrick, an AI coding assistant with agentic capabilities and file system access.
|
|
147
|
+
|
|
148
|
+
Current working directory: {self.tool_executor.working_dir}
|
|
149
|
+
|
|
150
|
+
# Agentic Behavior
|
|
151
|
+
|
|
152
|
+
You can ITERATE through multiple tool calls until a task is complete. You are not limited to a single response.
|
|
153
|
+
|
|
154
|
+
## Process
|
|
155
|
+
|
|
156
|
+
1. Call tools to gather information or make changes
|
|
157
|
+
2. Analyze the results
|
|
158
|
+
3. Continue calling tools as needed
|
|
159
|
+
4. Signal completion when done
|
|
160
|
+
|
|
161
|
+
# Completion Signal
|
|
162
|
+
|
|
163
|
+
When you've completed the task, say "TASK_COMPLETE" followed by a summary of what you accomplished.
|
|
164
|
+
|
|
165
|
+
# Tool Call Format (EXACT SYNTAX REQUIRED)
|
|
166
|
+
|
|
167
|
+
```tool_call
|
|
168
|
+
{{
|
|
169
|
+
"tool": "tool_name",
|
|
170
|
+
"parameters": {{
|
|
171
|
+
"param": "value"
|
|
172
|
+
}}
|
|
173
|
+
}}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
# Available Tools
|
|
177
|
+
|
|
178
|
+
{get_tools_prompt()}
|
|
179
|
+
|
|
180
|
+
# How to Explore Directories
|
|
181
|
+
|
|
182
|
+
To explore the full codebase structure:
|
|
183
|
+
- Use `list_files` with pattern `**/*.py` to list all Python files recursively
|
|
184
|
+
- Use `list_files` with pattern `**/*` to see ALL files and directories
|
|
185
|
+
- Use `run_bash` with `find . -type f` to list all files
|
|
186
|
+
- Use `run_bash` with `tree` or `ls -R` to see directory structure
|
|
187
|
+
|
|
188
|
+
Example - list all Python files:
|
|
189
|
+
```tool_call
|
|
190
|
+
{{
|
|
191
|
+
"tool": "list_files",
|
|
192
|
+
"parameters": {{
|
|
193
|
+
"pattern": "**/*.py"
|
|
194
|
+
}}
|
|
195
|
+
}}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
# Important Rules
|
|
199
|
+
|
|
200
|
+
1. **ITERATE**: Call tools immediately when needed, then analyze results and continue iterating
|
|
201
|
+
2. **MULTIPLE TOOLS**: You can call multiple tools per response
|
|
202
|
+
3. **READ BEFORE EDIT**: Always read a file before editing it
|
|
203
|
+
4. **EXPLORE THOROUGHLY**: Use `**/*` patterns to see all files in subdirectories
|
|
204
|
+
5. **SIGNAL COMPLETION**: Say "TASK_COMPLETE" when the task is done
|
|
205
|
+
6. **USE TOOLS IMMEDIATELY**: Don't ask permission - just call the tool
|
|
206
|
+
|
|
207
|
+
# Examples
|
|
208
|
+
|
|
209
|
+
User: "Read the config.pbtxt file"
|
|
210
|
+
Assistant: I'll read that file for you.
|
|
211
|
+
|
|
212
|
+
```tool_call
|
|
213
|
+
{{
|
|
214
|
+
"tool": "read_file",
|
|
215
|
+
"parameters": {{
|
|
216
|
+
"file_path": "config.pbtxt"
|
|
217
|
+
}}
|
|
218
|
+
}}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
User: "Create a new Python script called test.py with a hello world function"
|
|
222
|
+
Assistant: I'll create that file for you.
|
|
223
|
+
|
|
224
|
+
```tool_call
|
|
225
|
+
{{
|
|
226
|
+
"tool": "write_file",
|
|
227
|
+
"parameters": {{
|
|
228
|
+
"file_path": "test.py",
|
|
229
|
+
"content": "def hello_world():\\n print('Hello, World!')\\n\\nif __name__ == '__main__':\\n hello_world()\\n"
|
|
230
|
+
}}
|
|
231
|
+
}}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
User: "Read main.py and add logging to all functions"
|
|
235
|
+
Assistant: I'll first read the file to understand its structure.
|
|
236
|
+
|
|
237
|
+
```tool_call
|
|
238
|
+
{{
|
|
239
|
+
"tool": "read_file",
|
|
240
|
+
"parameters": {{
|
|
241
|
+
"file_path": "main.py"
|
|
242
|
+
}}
|
|
243
|
+
}}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
[After seeing the results, I'll add logging and continue iterating until done, then say TASK_COMPLETE]
|
|
247
|
+
""",
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
def _load_conversation(self, conversation_id: str) -> list:
|
|
252
|
+
"""Load a conversation by ID."""
|
|
253
|
+
data = self.config.load_conversation(conversation_id)
|
|
254
|
+
if data:
|
|
255
|
+
return data.get("messages", [])
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
def _save_conversation(self):
|
|
259
|
+
"""Save current conversation to disk."""
|
|
260
|
+
if self.config.get("auto_save_conversations", True):
|
|
261
|
+
metadata = {
|
|
262
|
+
"working_dir": str(self.tool_executor.working_dir),
|
|
263
|
+
"provider": self.provider.provider_name,
|
|
264
|
+
"model_name": self.provider.model_name,
|
|
265
|
+
"saved_at": datetime.now().isoformat(),
|
|
266
|
+
}
|
|
267
|
+
self.config.save_conversation(
|
|
268
|
+
self.conversation_id, self.messages, metadata
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def parse_tool_calls(self, text: str) -> list:
|
|
272
|
+
"""
|
|
273
|
+
Parse tool calls from LLM response.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
text: Response text from LLM
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
List of (tool_name, parameters) tuples
|
|
280
|
+
"""
|
|
281
|
+
tool_calls = []
|
|
282
|
+
|
|
283
|
+
# Primary parser: Find all ```tool_call blocks (correct format)
|
|
284
|
+
pattern = r"```tool_call\s*\n(.*?)\n```"
|
|
285
|
+
matches = re.findall(pattern, text, re.DOTALL)
|
|
286
|
+
|
|
287
|
+
for match in matches:
|
|
288
|
+
try:
|
|
289
|
+
tool_data = json.loads(match.strip())
|
|
290
|
+
tool_name = tool_data.get("tool")
|
|
291
|
+
parameters = tool_data.get("parameters", {})
|
|
292
|
+
if tool_name:
|
|
293
|
+
tool_calls.append((tool_name, parameters))
|
|
294
|
+
except json.JSONDecodeError as e:
|
|
295
|
+
console.print(f"[red]Failed to parse tool call: {e}[/red]")
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# Fallback parser: Look for standalone JSON objects that look like tool calls
|
|
299
|
+
# This handles cases where the LLM forgot the markdown fence
|
|
300
|
+
if not tool_calls:
|
|
301
|
+
# Look for JSON objects with "tool" and "parameters" keys
|
|
302
|
+
json_pattern = r'\{\s*"tool"\s*:\s*"([^"]+)"\s*,\s*"parameters"\s*:\s*\{[^}]*\}\s*\}'
|
|
303
|
+
json_matches = re.findall(json_pattern, text, re.DOTALL)
|
|
304
|
+
|
|
305
|
+
if json_matches:
|
|
306
|
+
console.print(
|
|
307
|
+
"[yellow]⚠ Warning: Detected tool call without proper markdown fence. "
|
|
308
|
+
"Parsing anyway, but please use ```tool_call format.[/yellow]"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Try to extract the full JSON objects
|
|
312
|
+
# More lenient pattern to capture the whole object
|
|
313
|
+
full_json_pattern = r'(\{\s*"tool"\s*:\s*"[^"]+"\s*,\s*"parameters"\s*:\s*\{.*?\}\s*\})'
|
|
314
|
+
for match in re.finditer(full_json_pattern, text, re.DOTALL):
|
|
315
|
+
try:
|
|
316
|
+
tool_data = json.loads(match.group(1))
|
|
317
|
+
tool_name = tool_data.get("tool")
|
|
318
|
+
parameters = tool_data.get("parameters", {})
|
|
319
|
+
if tool_name:
|
|
320
|
+
tool_calls.append((tool_name, parameters))
|
|
321
|
+
except json.JSONDecodeError:
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
return tool_calls
|
|
325
|
+
|
|
326
|
+
def display_streaming_response(self, chunks: list) -> str:
|
|
327
|
+
"""
|
|
328
|
+
Display streaming response and return full text.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
chunks: List of text chunks
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Full response text
|
|
335
|
+
"""
|
|
336
|
+
full_text = "".join(chunks)
|
|
337
|
+
|
|
338
|
+
# Split response around tool calls for better display
|
|
339
|
+
parts = re.split(r"(```tool_call.*?```)", full_text, flags=re.DOTALL)
|
|
340
|
+
|
|
341
|
+
for part in parts:
|
|
342
|
+
if part.startswith("```tool_call"):
|
|
343
|
+
# Display tool call in a panel
|
|
344
|
+
console.print(
|
|
345
|
+
Panel(part, title="Tool Call", border_style="cyan")
|
|
346
|
+
)
|
|
347
|
+
elif part.strip():
|
|
348
|
+
# Display regular text as markdown
|
|
349
|
+
console.print(Markdown(part))
|
|
350
|
+
|
|
351
|
+
return full_text
|
|
352
|
+
|
|
353
|
+
def run_conversation_turn(self, user_message: str) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Run one turn of the conversation with optimized execution strategy.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
user_message: User's message
|
|
359
|
+
"""
|
|
360
|
+
# Add user message to conversation
|
|
361
|
+
self.messages.append({"role": "user", "content": user_message})
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
# Step 1: Classify task (if enabled)
|
|
365
|
+
classification = None
|
|
366
|
+
if self.config.get("enable_task_classification", True):
|
|
367
|
+
classification = self.classifier.classify(
|
|
368
|
+
user_message, self.messages
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Step 2: Get execution strategy based on classification
|
|
372
|
+
exec_config = ExecutionStrategy.get_execution_config(
|
|
373
|
+
classification,
|
|
374
|
+
self.provider.provider_name,
|
|
375
|
+
self.provider.model_name,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
console.print(
|
|
379
|
+
f"[dim]→ Execution mode: {exec_config.mode} "
|
|
380
|
+
f"(model: {exec_config.model_tier}, "
|
|
381
|
+
f"max_iter: {exec_config.max_iterations})[/dim]"
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Step 3: Switch model if needed (and provider supports it)
|
|
385
|
+
current_model = self.provider.model_name
|
|
386
|
+
target_model = exec_config.hyperparameters.get("model")
|
|
387
|
+
|
|
388
|
+
if target_model and target_model != current_model:
|
|
389
|
+
try:
|
|
390
|
+
self.provider.set_model(target_model)
|
|
391
|
+
console.print(
|
|
392
|
+
f"[dim]→ Switched model: {current_model} → {target_model}[/dim]"
|
|
393
|
+
)
|
|
394
|
+
except Exception:
|
|
395
|
+
# Provider doesn't support model switching, continue with current
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
else:
|
|
399
|
+
# Classification disabled, use default complex config
|
|
400
|
+
exec_config = ExecutionStrategy.get_execution_config(
|
|
401
|
+
None, # Will trigger fallback
|
|
402
|
+
self.provider.provider_name,
|
|
403
|
+
self.provider.model_name,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Step 4: Execute based on mode
|
|
407
|
+
if exec_config.mode == "conversational":
|
|
408
|
+
# Conversational mode: single-turn response, no tools
|
|
409
|
+
self._run_conversational_turn(exec_config)
|
|
410
|
+
|
|
411
|
+
else:
|
|
412
|
+
# Agentic modes: use agent loop
|
|
413
|
+
self._run_agentic_turn(
|
|
414
|
+
classification, exec_config, user_message
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
except KeyboardInterrupt:
|
|
418
|
+
console.print("\n[yellow]Interrupted[/yellow]")
|
|
419
|
+
except Exception as e:
|
|
420
|
+
import traceback
|
|
421
|
+
|
|
422
|
+
console.print(f"\n[red]Error: {e}[/red]")
|
|
423
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
424
|
+
|
|
425
|
+
def _run_conversational_turn(self, exec_config):
|
|
426
|
+
"""
|
|
427
|
+
Handle conversational turn (no tools, single response).
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
exec_config: Execution configuration
|
|
431
|
+
"""
|
|
432
|
+
console.print("[bold cyan]Assistant:[/bold cyan]")
|
|
433
|
+
chunks = []
|
|
434
|
+
|
|
435
|
+
# Pass hyperparameters to provider
|
|
436
|
+
stream_options = exec_config.hyperparameters.copy()
|
|
437
|
+
|
|
438
|
+
for chunk in self.provider.generate_streaming(
|
|
439
|
+
self.messages, stream_options=stream_options
|
|
440
|
+
):
|
|
441
|
+
console.print(chunk, end="")
|
|
442
|
+
chunks.append(chunk)
|
|
443
|
+
|
|
444
|
+
console.print("\n")
|
|
445
|
+
|
|
446
|
+
# Add response to conversation
|
|
447
|
+
response_text = "".join(chunks)
|
|
448
|
+
self.messages.append({"role": "assistant", "content": response_text})
|
|
449
|
+
|
|
450
|
+
def _run_agentic_turn(self, classification, exec_config, user_message):
|
|
451
|
+
"""
|
|
452
|
+
Handle agentic turn (with tools and iterations).
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
classification: Task classification
|
|
456
|
+
exec_config: Execution configuration
|
|
457
|
+
user_message: Original user message
|
|
458
|
+
"""
|
|
459
|
+
# Check if planning phase should be offered
|
|
460
|
+
if (
|
|
461
|
+
exec_config.use_planning
|
|
462
|
+
and classification
|
|
463
|
+
and classification.complexity == "COMPLEX"
|
|
464
|
+
and self.config.get("enable_planning_phase", True)
|
|
465
|
+
):
|
|
466
|
+
response = Prompt.ask(
|
|
467
|
+
"[bold yellow]This looks complex. Create a plan first?[/bold yellow]",
|
|
468
|
+
choices=["yes", "no"],
|
|
469
|
+
default="yes",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if response == "yes":
|
|
473
|
+
# Execute planning phase
|
|
474
|
+
plan = self.planning_phase.execute_planning(
|
|
475
|
+
user_message, self.messages
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Get user approval
|
|
479
|
+
approval = self.planning_phase.get_user_approval(plan)
|
|
480
|
+
|
|
481
|
+
if not approval.get("approved", False):
|
|
482
|
+
console.print("[yellow]Task cancelled by user[/yellow]")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
# If modifications requested, add them to conversation
|
|
486
|
+
if "modifications" in approval:
|
|
487
|
+
self.messages.append(
|
|
488
|
+
{
|
|
489
|
+
"role": "user",
|
|
490
|
+
"content": f"Plan modifications: {approval['modifications']}",
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Update agent loop max iterations for this turn
|
|
495
|
+
original_max_iterations = self.agent_loop.max_iterations
|
|
496
|
+
self.agent_loop.max_iterations = exec_config.max_iterations
|
|
497
|
+
|
|
498
|
+
# Run agent loop with hyperparameters
|
|
499
|
+
self.agent_loop.stream_options = exec_config.hyperparameters
|
|
500
|
+
|
|
501
|
+
result = self.agent_loop.run(
|
|
502
|
+
messages=self.messages,
|
|
503
|
+
tool_parser=self.parse_tool_calls,
|
|
504
|
+
display_callback=None,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Restore original max iterations
|
|
508
|
+
self.agent_loop.max_iterations = original_max_iterations
|
|
509
|
+
|
|
510
|
+
# Display result summary
|
|
511
|
+
if result["success"]:
|
|
512
|
+
console.print(
|
|
513
|
+
f"\n[dim]Completed in {result['iterations']} iteration(s) "
|
|
514
|
+
f"with {result['tool_calls']} tool call(s)[/dim]"
|
|
515
|
+
)
|
|
516
|
+
else:
|
|
517
|
+
console.print(f"\n[red]Error: {result.get('error')}[/red]")
|
|
518
|
+
|
|
519
|
+
def run(self):
|
|
520
|
+
"""Run the interactive CLI."""
|
|
521
|
+
console.print(
|
|
522
|
+
Panel.fit(
|
|
523
|
+
"[bold cyan]Kubrick CLI[/bold cyan]\n"
|
|
524
|
+
f"Working directory: {self.tool_executor.working_dir}\n"
|
|
525
|
+
f"Conversation ID: {self.conversation_id}\n"
|
|
526
|
+
"Type your questions or commands. Use 'exit' or 'quit' to exit.\n"
|
|
527
|
+
"Type '/help' to see available commands.",
|
|
528
|
+
border_style="cyan",
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Check provider health
|
|
533
|
+
if not self.provider.is_healthy():
|
|
534
|
+
console.print(
|
|
535
|
+
f"[red]Warning: Cannot connect to {self.provider.provider_name} provider[/red]"
|
|
536
|
+
)
|
|
537
|
+
console.print(
|
|
538
|
+
"[yellow]Please check your provider configuration and connectivity.[/yellow]"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
while True:
|
|
542
|
+
try:
|
|
543
|
+
user_input = Prompt.ask("\n[bold green]You[/bold green]")
|
|
544
|
+
|
|
545
|
+
# Reset interrupt count when user enters a message
|
|
546
|
+
self.interrupt_count = 0
|
|
547
|
+
|
|
548
|
+
if not user_input.strip():
|
|
549
|
+
continue
|
|
550
|
+
|
|
551
|
+
if user_input.lower() in ["exit", "quit", "q"]:
|
|
552
|
+
# Save conversation before exiting
|
|
553
|
+
self._save_conversation()
|
|
554
|
+
console.print(
|
|
555
|
+
f"[cyan]Conversation saved as {self.conversation_id}[/cyan]"
|
|
556
|
+
)
|
|
557
|
+
console.print("[cyan]Goodbye![/cyan]")
|
|
558
|
+
break
|
|
559
|
+
|
|
560
|
+
# Handle special commands
|
|
561
|
+
if user_input.startswith("/"):
|
|
562
|
+
self._handle_command(user_input)
|
|
563
|
+
continue
|
|
564
|
+
|
|
565
|
+
self.run_conversation_turn(user_input)
|
|
566
|
+
|
|
567
|
+
# Auto-save after each turn
|
|
568
|
+
self._save_conversation()
|
|
569
|
+
|
|
570
|
+
except KeyboardInterrupt:
|
|
571
|
+
self.interrupt_count += 1
|
|
572
|
+
|
|
573
|
+
if self.interrupt_count == 1:
|
|
574
|
+
# First Ctrl+C: Clear prompt
|
|
575
|
+
console.print("\n[yellow]^C (Press again to start new conversation, once more to exit)[/yellow]")
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
elif self.interrupt_count == 2:
|
|
579
|
+
# Second Ctrl+C: Start new conversation
|
|
580
|
+
console.print("\n[yellow]Starting new conversation...[/yellow]")
|
|
581
|
+
self._save_conversation()
|
|
582
|
+
|
|
583
|
+
# Reset conversation
|
|
584
|
+
self.conversation_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
585
|
+
self.messages = self._get_initial_messages()
|
|
586
|
+
|
|
587
|
+
console.print(f"[cyan]New conversation ID: {self.conversation_id}[/cyan]")
|
|
588
|
+
console.print("[dim]Press Ctrl+C once more to exit[/dim]")
|
|
589
|
+
continue
|
|
590
|
+
|
|
591
|
+
else:
|
|
592
|
+
# Third Ctrl+C: Exit
|
|
593
|
+
self._save_conversation()
|
|
594
|
+
console.print(
|
|
595
|
+
f"\n[cyan]Conversation saved as {self.conversation_id}[/cyan]"
|
|
596
|
+
)
|
|
597
|
+
console.print("[cyan]Goodbye![/cyan]")
|
|
598
|
+
break
|
|
599
|
+
|
|
600
|
+
except Exception as e:
|
|
601
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
602
|
+
|
|
603
|
+
def _handle_command(self, command: str):
|
|
604
|
+
"""Handle special CLI commands."""
|
|
605
|
+
parts = command.strip().split()
|
|
606
|
+
cmd = parts[0].lower()
|
|
607
|
+
|
|
608
|
+
if cmd == "/save":
|
|
609
|
+
self._save_conversation()
|
|
610
|
+
console.print(
|
|
611
|
+
f"[green]Conversation saved as {self.conversation_id}[/green]"
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
elif cmd == "/list":
|
|
615
|
+
limit = int(parts[1]) if len(parts) > 1 else 20
|
|
616
|
+
conversations = self.config.list_conversations(limit=limit)
|
|
617
|
+
|
|
618
|
+
if not conversations:
|
|
619
|
+
console.print("[yellow]No saved conversations found[/yellow]")
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
table = Table(title="Saved Conversations")
|
|
623
|
+
table.add_column("ID", style="cyan")
|
|
624
|
+
table.add_column("Messages", justify="right", style="green")
|
|
625
|
+
table.add_column("Working Dir", style="dim")
|
|
626
|
+
table.add_column("Modified", style="yellow")
|
|
627
|
+
|
|
628
|
+
for conv in conversations:
|
|
629
|
+
conv_id = conv["id"]
|
|
630
|
+
msg_count = str(conv["message_count"])
|
|
631
|
+
working_dir = conv["metadata"].get("working_dir", "N/A")
|
|
632
|
+
modified = datetime.fromtimestamp(conv["modified"]).strftime(
|
|
633
|
+
"%Y-%m-%d %H:%M"
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
table.add_row(conv_id, msg_count, working_dir, modified)
|
|
637
|
+
|
|
638
|
+
console.print(table)
|
|
639
|
+
|
|
640
|
+
elif cmd == "/config":
|
|
641
|
+
if len(parts) == 1:
|
|
642
|
+
# Show current config
|
|
643
|
+
config_data = self.config.get_all()
|
|
644
|
+
table = Table(title="Current Configuration")
|
|
645
|
+
table.add_column("Setting", style="cyan")
|
|
646
|
+
table.add_column("Value", style="green")
|
|
647
|
+
|
|
648
|
+
for key, value in config_data.items():
|
|
649
|
+
table.add_row(key, str(value))
|
|
650
|
+
|
|
651
|
+
console.print(table)
|
|
652
|
+
|
|
653
|
+
elif len(parts) == 3:
|
|
654
|
+
# Set config value
|
|
655
|
+
key = parts[1]
|
|
656
|
+
value = parts[2]
|
|
657
|
+
|
|
658
|
+
# Try to parse as JSON for booleans/numbers
|
|
659
|
+
try:
|
|
660
|
+
value = json.loads(value)
|
|
661
|
+
except json.JSONDecodeError:
|
|
662
|
+
pass # Keep as string
|
|
663
|
+
|
|
664
|
+
self.config.set(key, value)
|
|
665
|
+
console.print(f"[green]Set {key} = {value}[/green]")
|
|
666
|
+
|
|
667
|
+
else:
|
|
668
|
+
console.print("[yellow]Usage: /config [key value][/yellow]")
|
|
669
|
+
|
|
670
|
+
elif cmd == "/delete":
|
|
671
|
+
if len(parts) < 2:
|
|
672
|
+
console.print(
|
|
673
|
+
"[yellow]Usage: /delete <conversation_id>[/yellow]"
|
|
674
|
+
)
|
|
675
|
+
return
|
|
676
|
+
|
|
677
|
+
conv_id = parts[1]
|
|
678
|
+
if self.config.delete_conversation(conv_id):
|
|
679
|
+
console.print(f"[green]Deleted conversation {conv_id}[/green]")
|
|
680
|
+
else:
|
|
681
|
+
console.print(
|
|
682
|
+
f"[yellow]Conversation {conv_id} not found[/yellow]"
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
elif cmd == "/debug":
|
|
686
|
+
# Show debug information
|
|
687
|
+
table = Table(title="Debug Information")
|
|
688
|
+
table.add_column("Item", style="cyan")
|
|
689
|
+
table.add_column("Value", style="green")
|
|
690
|
+
|
|
691
|
+
table.add_row("Conversation ID", self.conversation_id)
|
|
692
|
+
table.add_row("Messages Count", str(len(self.messages)))
|
|
693
|
+
table.add_row("Working Dir", str(self.tool_executor.working_dir))
|
|
694
|
+
table.add_row("Provider", self.provider.provider_name)
|
|
695
|
+
table.add_row("Model Name", self.provider.model_name)
|
|
696
|
+
|
|
697
|
+
console.print(table)
|
|
698
|
+
|
|
699
|
+
# Show system prompt
|
|
700
|
+
if len(parts) > 1 and parts[1] == "prompt":
|
|
701
|
+
console.print("\n[bold cyan]System Prompt:[/bold cyan]")
|
|
702
|
+
system_msg = next(
|
|
703
|
+
(m for m in self.messages if m["role"] == "system"), None
|
|
704
|
+
)
|
|
705
|
+
if system_msg:
|
|
706
|
+
console.print(
|
|
707
|
+
Panel(system_msg["content"], border_style="dim")
|
|
708
|
+
)
|
|
709
|
+
else:
|
|
710
|
+
console.print("[yellow]No system message found[/yellow]")
|
|
711
|
+
|
|
712
|
+
elif cmd == "/help":
|
|
713
|
+
console.print("""
|
|
714
|
+
[bold cyan]Available Commands:[/bold cyan]
|
|
715
|
+
|
|
716
|
+
[green]/save[/green] - Save the current conversation
|
|
717
|
+
[green]/list [N][/green] - List saved conversations (default: 20)
|
|
718
|
+
[green]/config[/green] - Show current configuration
|
|
719
|
+
[green]/config KEY VALUE[/green] - Set a configuration value
|
|
720
|
+
[green]/delete ID[/green] - Delete a conversation
|
|
721
|
+
[green]/debug[/green] - Show debug information
|
|
722
|
+
[green]/debug prompt[/green] - Show the system prompt
|
|
723
|
+
[green]/help[/green] - Show this help message
|
|
724
|
+
""")
|
|
725
|
+
|
|
726
|
+
else:
|
|
727
|
+
console.print(f"[yellow]Unknown command: {cmd}[/yellow]")
|
|
728
|
+
console.print(
|
|
729
|
+
"[dim]Available commands: /save, /list, /config, /delete, /debug, /help[/dim]"
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def main():
|
|
734
|
+
"""Main entry point."""
|
|
735
|
+
# Initialize config (creates ~/.kubrick if needed, runs setup wizard on first run)
|
|
736
|
+
config = KubrickConfig()
|
|
737
|
+
|
|
738
|
+
parser = argparse.ArgumentParser(
|
|
739
|
+
description="Kubrick - AI-assisted coding CLI with agentic capabilities",
|
|
740
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
741
|
+
epilog="""
|
|
742
|
+
Special Commands:
|
|
743
|
+
/save Save the current conversation
|
|
744
|
+
/list [N] List saved conversations (default: 20)
|
|
745
|
+
/config Show current configuration
|
|
746
|
+
/config KEY VALUE Set a configuration value
|
|
747
|
+
/delete ID Delete a conversation
|
|
748
|
+
|
|
749
|
+
Examples:
|
|
750
|
+
kubrick # Start new conversation with configured provider
|
|
751
|
+
kubrick --load 20240118_143022 # Load a previous conversation
|
|
752
|
+
kubrick --working-dir /path/to/project # Set working directory
|
|
753
|
+
kubrick --provider openai # Override provider for this session
|
|
754
|
+
""",
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
parser.add_argument(
|
|
758
|
+
"--working-dir",
|
|
759
|
+
default=None,
|
|
760
|
+
help="Working directory for file operations (default: current directory)",
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
parser.add_argument(
|
|
764
|
+
"--load",
|
|
765
|
+
"--load-conversation",
|
|
766
|
+
dest="conversation_id",
|
|
767
|
+
default=None,
|
|
768
|
+
help="Load a previous conversation by ID",
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
parser.add_argument(
|
|
772
|
+
"--provider",
|
|
773
|
+
default=None,
|
|
774
|
+
choices=["triton", "openai", "anthropic"],
|
|
775
|
+
help="Override configured provider for this session",
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
args = parser.parse_args()
|
|
779
|
+
|
|
780
|
+
# Create and run CLI
|
|
781
|
+
try:
|
|
782
|
+
cli = KubrickCLI(
|
|
783
|
+
config=config,
|
|
784
|
+
working_dir=args.working_dir,
|
|
785
|
+
conversation_id=args.conversation_id,
|
|
786
|
+
provider_override=args.provider,
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
cli.run()
|
|
790
|
+
except Exception as e:
|
|
791
|
+
console.print(f"[red]Failed to start Kubrick: {e}[/red]")
|
|
792
|
+
import sys
|
|
793
|
+
sys.exit(1)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
if __name__ == "__main__":
|
|
797
|
+
main()
|