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.
@@ -0,0 +1,47 @@
1
+ """Kubrick CLI - AI-assisted coding tool with agentic capabilities."""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ from .agent_loop import AgentLoop, CompletionDetector
6
+ from .classifier import TaskClassification, TaskClassifier
7
+ from .config import KubrickConfig
8
+ from .display import DisplayManager
9
+ from .execution_strategy import ExecutionConfig, ExecutionStrategy
10
+ from .main import KubrickCLI
11
+ from .planning import PlanningPhase
12
+ from .progress import ProgressTracker
13
+ from .providers.base import ProviderAdapter
14
+ from .providers.factory import ProviderFactory
15
+ from .providers.triton_provider import TritonProvider
16
+ from .providers.openai_provider import OpenAIProvider
17
+ from .providers.anthropic_provider import AnthropicProvider
18
+ from .safety import SafetyConfig, SafetyManager
19
+ from .scheduler import ToolScheduler
20
+ from .setup_wizard import SetupWizard
21
+ from .tools import ToolExecutor
22
+ from .triton_client import TritonLLMClient
23
+
24
+ __all__ = [
25
+ "AgentLoop",
26
+ "AnthropicProvider",
27
+ "CompletionDetector",
28
+ "DisplayManager",
29
+ "ExecutionConfig",
30
+ "ExecutionStrategy",
31
+ "KubrickCLI",
32
+ "KubrickConfig",
33
+ "OpenAIProvider",
34
+ "PlanningPhase",
35
+ "ProgressTracker",
36
+ "ProviderAdapter",
37
+ "ProviderFactory",
38
+ "SafetyConfig",
39
+ "SafetyManager",
40
+ "SetupWizard",
41
+ "TaskClassification",
42
+ "TaskClassifier",
43
+ "ToolExecutor",
44
+ "ToolScheduler",
45
+ "TritonLLMClient",
46
+ "TritonProvider",
47
+ ]
@@ -0,0 +1,307 @@
1
+ """Multi-step agentic execution loop with completion detection."""
2
+
3
+ import re
4
+ from typing import Dict, Iterator, List, Optional, Tuple
5
+
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+ # Import display manager (will be None if not available)
11
+ try:
12
+ from .display import DisplayManager
13
+ except ImportError:
14
+ DisplayManager = None
15
+
16
+ # Import scheduler (will be None if not available)
17
+ try:
18
+ from .scheduler import ToolScheduler
19
+ except ImportError:
20
+ ToolScheduler = None
21
+
22
+
23
+ class CompletionDetector:
24
+ """Detects when an agent has completed its task."""
25
+
26
+ # Explicit completion markers the agent can use
27
+ COMPLETION_MARKERS = [
28
+ "TASK_COMPLETE",
29
+ "PLAN_COMPLETE",
30
+ "[COMPLETE]",
31
+ "[DONE]",
32
+ ]
33
+
34
+ @staticmethod
35
+ def is_complete(
36
+ response_text: str,
37
+ has_tool_calls: bool,
38
+ iteration: int,
39
+ max_iterations: int,
40
+ ) -> Tuple[bool, str]:
41
+ """
42
+ Determine if the agent has completed its task.
43
+
44
+ Args:
45
+ response_text: The agent's response text
46
+ has_tool_calls: Whether the response contains tool calls
47
+ iteration: Current iteration number
48
+ max_iterations: Maximum allowed iterations
49
+
50
+ Returns:
51
+ Tuple of (is_complete, reason)
52
+ """
53
+ # Check for explicit completion markers
54
+ for marker in CompletionDetector.COMPLETION_MARKERS:
55
+ if marker in response_text:
56
+ return True, f"explicit_marker:{marker}"
57
+
58
+ # Max iterations reached (fallback safety)
59
+ if iteration >= max_iterations:
60
+ return True, "max_iterations_reached"
61
+
62
+ # If no tool calls and response looks conclusive
63
+ if not has_tool_calls:
64
+ # Check if response appears to be a final summary/conclusion
65
+ if CompletionDetector._looks_conclusive(response_text):
66
+ return True, "conclusive_response"
67
+
68
+ # Otherwise, continue
69
+ return False, "continuing"
70
+
71
+ @staticmethod
72
+ def _looks_conclusive(text: str) -> bool:
73
+ """
74
+ Check if text looks like a conclusive response.
75
+
76
+ This is a heuristic to detect when the agent is done without
77
+ explicitly saying so.
78
+ """
79
+ text_lower = text.lower()
80
+
81
+ # Look for summary/conclusion indicators
82
+ conclusive_patterns = [
83
+ r"\b(done|completed|finished|ready)\b",
84
+ r"\b(successfully|all set|good to go)\b",
85
+ r"\bhere(?:'s| is) (?:the |a )?(?:summary|result)",
86
+ r"\b(?:task|work|changes) (?:is |are )?(?:complete|done|finished)",
87
+ r"\blet me know if you need",
88
+ r"\bthat(?:'s| should do it)",
89
+ r"\beverything(?:'s| is) (?:set|ready|done)",
90
+ ]
91
+
92
+ for pattern in conclusive_patterns:
93
+ if re.search(pattern, text_lower):
94
+ return True
95
+
96
+ return False
97
+
98
+
99
+ class AgentLoop:
100
+ """
101
+ Multi-step agentic execution loop.
102
+
103
+ Runs iteratively until the task is complete or max iterations reached.
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ llm_client,
109
+ tool_executor,
110
+ max_iterations: int = 15,
111
+ max_tools_per_turn: int = 5,
112
+ timeout_seconds: int = 600,
113
+ stream_options: Dict = None,
114
+ display_manager=None,
115
+ tool_scheduler=None,
116
+ ):
117
+ """
118
+ Initialize the agent loop.
119
+
120
+ Args:
121
+ llm_client: LLM client instance (e.g., TritonLLMClient)
122
+ tool_executor: Tool executor instance
123
+ max_iterations: Maximum number of iterations
124
+ max_tools_per_turn: Maximum tools per turn
125
+ timeout_seconds: Total timeout in seconds
126
+ stream_options: Optional streaming options
127
+ display_manager: Optional DisplayManager instance for natural language display
128
+ tool_scheduler: Optional ToolScheduler for parallel execution
129
+ """
130
+ self.llm_client = llm_client
131
+ self.tool_executor = tool_executor
132
+ self.max_iterations = max_iterations
133
+ self.max_tools_per_turn = max_tools_per_turn
134
+ self.timeout_seconds = timeout_seconds
135
+ self.stream_options = stream_options or {}
136
+ self.display_manager = display_manager
137
+ self.tool_scheduler = tool_scheduler
138
+
139
+ def run(
140
+ self,
141
+ messages: List[Dict],
142
+ tool_parser,
143
+ display_callback=None,
144
+ ) -> Dict:
145
+ """
146
+ Run the agentic loop.
147
+
148
+ Args:
149
+ messages: Conversation messages (will be modified in-place)
150
+ tool_parser: Function to parse tool calls from text
151
+ display_callback: Optional callback for displaying streaming response
152
+
153
+ Returns:
154
+ Dict with execution results and metadata
155
+ """
156
+ iteration = 0
157
+ total_tool_calls = 0
158
+
159
+ while iteration < self.max_iterations:
160
+ iteration += 1
161
+
162
+ console.print(
163
+ f"\n[dim]→ Agent iteration {iteration}/{self.max_iterations}[/dim]"
164
+ )
165
+
166
+ # Get LLM response
167
+ console.print("[bold cyan]Assistant:[/bold cyan]")
168
+ chunks = []
169
+
170
+ try:
171
+ for chunk in self.llm_client.generate_streaming(
172
+ messages, stream_options=self.stream_options
173
+ ):
174
+ console.print(chunk, end="")
175
+ chunks.append(chunk)
176
+
177
+ console.print("\n")
178
+
179
+ except Exception as e:
180
+ console.print(f"\n[red]Error during LLM generation: {e}[/red]")
181
+ return {
182
+ "success": False,
183
+ "error": str(e),
184
+ "iterations": iteration,
185
+ "tool_calls": total_tool_calls,
186
+ }
187
+
188
+ # Get full response
189
+ response_text = "".join(chunks)
190
+
191
+ # Add to conversation
192
+ messages.append({"role": "assistant", "content": response_text})
193
+
194
+ # Display with custom callback if provided
195
+ if display_callback:
196
+ display_callback(response_text)
197
+
198
+ # Parse tool calls
199
+ tool_calls = tool_parser(response_text)
200
+
201
+ # Check completion
202
+ is_complete, reason = CompletionDetector.is_complete(
203
+ response_text=response_text,
204
+ has_tool_calls=len(tool_calls) > 0,
205
+ iteration=iteration,
206
+ max_iterations=self.max_iterations,
207
+ )
208
+
209
+ if is_complete:
210
+ console.print(f"\n[green]✓ Task complete ({reason})[/green]")
211
+ return {
212
+ "success": True,
213
+ "completion_reason": reason,
214
+ "iterations": iteration,
215
+ "tool_calls": total_tool_calls,
216
+ }
217
+
218
+ # Execute tool calls if any
219
+ if tool_calls:
220
+ # Enforce max tools per turn
221
+ if len(tool_calls) > self.max_tools_per_turn:
222
+ console.print(
223
+ f"[yellow]⚠ Too many tool calls ({len(tool_calls)}), "
224
+ f"limiting to {self.max_tools_per_turn}[/yellow]"
225
+ )
226
+ tool_calls = tool_calls[: self.max_tools_per_turn]
227
+
228
+ console.print(
229
+ f"\n[yellow]Executing {len(tool_calls)} tool(s)...[/yellow]\n"
230
+ )
231
+
232
+ # Execute tools (with or without scheduler)
233
+ if self.tool_scheduler and len(tool_calls) > 1:
234
+ # Use scheduler for parallel execution
235
+ execution_results = self.tool_scheduler.execute_tools(
236
+ tool_calls
237
+ )
238
+ else:
239
+ # Execute sequentially
240
+ execution_results = [
241
+ self.tool_executor.execute(tool_name, params)
242
+ for tool_name, params in tool_calls
243
+ ]
244
+
245
+ # Display and collect results
246
+ tool_results = []
247
+ for (tool_name, parameters), result in zip(
248
+ tool_calls, execution_results
249
+ ):
250
+ # Display tool call and result
251
+ if self.display_manager:
252
+ self.display_manager.display_tool_call(
253
+ tool_name, parameters
254
+ )
255
+ self.display_manager.display_tool_result(
256
+ tool_name, result, result["success"]
257
+ )
258
+ else:
259
+ console.print(f"[cyan]→ Called {tool_name}[/cyan]")
260
+ if result["success"]:
261
+ console.print(
262
+ f"[green]✓ {tool_name} succeeded[/green]"
263
+ )
264
+ else:
265
+ console.print(
266
+ f"[red]✗ {tool_name} failed: {result['error']}[/red]"
267
+ )
268
+
269
+ # Add to results
270
+ if result["success"]:
271
+ tool_results.append(
272
+ f"Tool: {tool_name}\nResult: {result['result']}"
273
+ )
274
+ else:
275
+ tool_results.append(
276
+ f"Tool: {tool_name}\nError: {result['error']}"
277
+ )
278
+
279
+ total_tool_calls += 1
280
+
281
+ # Add tool results to conversation
282
+ tool_results_text = "\n\n".join(tool_results)
283
+ messages.append(
284
+ {
285
+ "role": "user",
286
+ "content": f"Tool execution results:\n\n{tool_results_text}",
287
+ }
288
+ )
289
+
290
+ # Continue to next iteration
291
+ continue
292
+
293
+ # If no tool calls and not complete, something might be wrong
294
+ console.print(
295
+ "[yellow]⚠ No tool calls and task not marked complete. Continuing...[/yellow]"
296
+ )
297
+
298
+ # Max iterations reached
299
+ console.print(
300
+ f"\n[yellow]⚠ Max iterations ({self.max_iterations}) reached[/yellow]"
301
+ )
302
+ return {
303
+ "success": True,
304
+ "completion_reason": "max_iterations",
305
+ "iterations": iteration,
306
+ "tool_calls": total_tool_calls,
307
+ }
@@ -0,0 +1,198 @@
1
+ """Task complexity classification for intelligent routing."""
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Dict, List
6
+
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+
11
+
12
+ @dataclass
13
+ class TaskClassification:
14
+ """Result of task classification."""
15
+
16
+ complexity: str # CONVERSATIONAL, SIMPLE, COMPLEX
17
+ reasoning: str
18
+ estimated_tool_calls: int
19
+ requires_tools: bool
20
+
21
+
22
+ class TaskClassifier:
23
+ """
24
+ Classifies tasks into three tiers to determine execution strategy.
25
+
26
+ CONVERSATIONAL:
27
+ - Greetings, questions, general chat
28
+ - No tool calls needed
29
+ - Single-turn response sufficient
30
+
31
+ SIMPLE:
32
+ - Single file operations
33
+ - Clear, specific scope
34
+ - Estimated 1-5 tool calls
35
+ - Low iteration count (3-5)
36
+
37
+ COMPLEX:
38
+ - Multi-file operations
39
+ - Architectural changes
40
+ - Uncertain scope
41
+ - Estimated >5 tool calls
42
+ - Full iteration count (15)
43
+ """
44
+
45
+ def __init__(self, llm_client):
46
+ """
47
+ Initialize task classifier.
48
+
49
+ Args:
50
+ llm_client: LLM client instance for classification
51
+ """
52
+ self.llm_client = llm_client
53
+
54
+ def classify(
55
+ self, user_message: str, conversation_history: List[Dict] = None
56
+ ) -> TaskClassification:
57
+ """
58
+ Classify a task into CONVERSATIONAL, SIMPLE, or COMPLEX.
59
+
60
+ Args:
61
+ user_message: The user's task request
62
+ conversation_history: Optional conversation context
63
+
64
+ Returns:
65
+ TaskClassification object with detailed classification
66
+ """
67
+ # Build classification prompt
68
+ classification_messages = [
69
+ {
70
+ "role": "system",
71
+ "content": """You are a task complexity classifier for a coding assistant.
72
+
73
+ Your job is to classify tasks into three tiers: CONVERSATIONAL, SIMPLE, or COMPLEX.
74
+
75
+ # Classification Criteria
76
+
77
+ **CONVERSATIONAL:**
78
+ - Greetings, questions, general chat
79
+ - No file operations or code changes needed
80
+ - No tools required
81
+ - Estimated 0 tool calls
82
+ - Examples:
83
+ - "Hi", "Hello", "How are you?"
84
+ - "What can you do?"
85
+ - "Explain what async/await means"
86
+ - "Tell me about this project"
87
+
88
+ **SIMPLE:**
89
+ - Single file operations (read, write, edit one file)
90
+ - Clear, specific scope with well-defined requirements
91
+ - Estimated 1-5 tool calls
92
+ - Examples:
93
+ - "Read config.py"
94
+ - "Create a hello world script"
95
+ - "Fix the typo in line 42 of main.py"
96
+ - "List all Python files"
97
+
98
+ **COMPLEX:**
99
+ - Multi-file operations affecting multiple files
100
+ - Architectural changes or refactoring
101
+ - Uncertain scope requiring exploration
102
+ - Estimated >5 tool calls
103
+ - Examples:
104
+ - "Add logging to all Python files"
105
+ - "Refactor the authentication system"
106
+ - "Implement a new feature"
107
+ - "Debug why the tests are failing"
108
+
109
+ # Response Format
110
+
111
+ Respond with ONLY a JSON object:
112
+ ```json
113
+ {
114
+ "complexity": "CONVERSATIONAL",
115
+ "reasoning": "Brief explanation why",
116
+ "estimated_tool_calls": 0,
117
+ "requires_tools": false
118
+ }
119
+ ```
120
+
121
+ OR
122
+
123
+ ```json
124
+ {
125
+ "complexity": "SIMPLE",
126
+ "reasoning": "Brief explanation why",
127
+ "estimated_tool_calls": 2,
128
+ "requires_tools": true
129
+ }
130
+ ```
131
+
132
+ OR
133
+
134
+ ```json
135
+ {
136
+ "complexity": "COMPLEX",
137
+ "reasoning": "Brief explanation why",
138
+ "estimated_tool_calls": 10,
139
+ "requires_tools": true
140
+ }
141
+ ```
142
+
143
+ Respond with ONLY the JSON object, no other text.""",
144
+ },
145
+ {"role": "user", "content": user_message},
146
+ ]
147
+
148
+ try:
149
+ # Get classification from LLM (non-streaming for speed)
150
+ response = self.llm_client.generate(
151
+ classification_messages, stream_options={}
152
+ )
153
+
154
+ # Parse JSON response
155
+ json_start = response.find("{")
156
+ json_end = response.rfind("}") + 1
157
+
158
+ if json_start >= 0 and json_end > json_start:
159
+ json_str = response[json_start:json_end]
160
+ result = json.loads(json_str)
161
+
162
+ complexity = result.get("complexity", "SIMPLE").upper()
163
+ reasoning = result.get("reasoning", "No reasoning provided")
164
+ estimated_tool_calls = result.get("estimated_tool_calls", 0)
165
+ requires_tools = result.get("requires_tools", False)
166
+
167
+ console.print(
168
+ f"[dim]→ Task classified as {complexity}: {reasoning}[/dim]"
169
+ )
170
+
171
+ return TaskClassification(
172
+ complexity=complexity,
173
+ reasoning=reasoning,
174
+ estimated_tool_calls=estimated_tool_calls,
175
+ requires_tools=requires_tools,
176
+ )
177
+
178
+ # Fallback: default to SIMPLE if parsing fails
179
+ console.print(
180
+ "[yellow]⚠ Classification parsing failed, defaulting to SIMPLE[/yellow]"
181
+ )
182
+ return TaskClassification(
183
+ complexity="SIMPLE",
184
+ reasoning="Parsing failed",
185
+ estimated_tool_calls=3,
186
+ requires_tools=True,
187
+ )
188
+
189
+ except Exception as e:
190
+ console.print(
191
+ f"[yellow]⚠ Classification error ({e}), defaulting to SIMPLE[/yellow]"
192
+ )
193
+ return TaskClassification(
194
+ complexity="SIMPLE",
195
+ reasoning=f"Error: {e}",
196
+ estimated_tool_calls=3,
197
+ requires_tools=True,
198
+ )