kubrick-cli 0.1.4__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 +274 -0
- kubrick_cli/classifier.py +194 -0
- kubrick_cli/config.py +247 -0
- kubrick_cli/display.py +154 -0
- kubrick_cli/execution_strategy.py +195 -0
- kubrick_cli/main.py +806 -0
- kubrick_cli/planning.py +319 -0
- kubrick_cli/progress.py +162 -0
- kubrick_cli/providers/__init__.py +6 -0
- kubrick_cli/providers/anthropic_provider.py +209 -0
- kubrick_cli/providers/base.py +136 -0
- kubrick_cli/providers/factory.py +161 -0
- kubrick_cli/providers/openai_provider.py +181 -0
- kubrick_cli/providers/triton_provider.py +96 -0
- kubrick_cli/safety.py +204 -0
- kubrick_cli/scheduler.py +183 -0
- kubrick_cli/setup_wizard.py +161 -0
- kubrick_cli/tools.py +400 -0
- kubrick_cli/triton_client.py +177 -0
- kubrick_cli-0.1.4.dist-info/METADATA +137 -0
- kubrick_cli-0.1.4.dist-info/RECORD +26 -0
- kubrick_cli-0.1.4.dist-info/WHEEL +5 -0
- kubrick_cli-0.1.4.dist-info/entry_points.txt +2 -0
- kubrick_cli-0.1.4.dist-info/licenses/LICENSE +21 -0
- kubrick_cli-0.1.4.dist-info/top_level.txt +1 -0
kubrick_cli/planning.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Planning phase for complex tasks with read-only exploration."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.markdown import Markdown
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.prompt import Prompt
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Read-only tools allowed in planning mode
|
|
14
|
+
PLANNING_ALLOWED_TOOLS = {
|
|
15
|
+
"read_file",
|
|
16
|
+
"list_files",
|
|
17
|
+
"search_files",
|
|
18
|
+
# run_bash is allowed but will be restricted to read-only commands
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Dangerous bash commands that are never allowed in planning
|
|
22
|
+
DANGEROUS_BASH_PATTERNS = [
|
|
23
|
+
"rm ",
|
|
24
|
+
"mv ",
|
|
25
|
+
"cp ",
|
|
26
|
+
"chmod",
|
|
27
|
+
"chown",
|
|
28
|
+
"sudo",
|
|
29
|
+
"> ",
|
|
30
|
+
">>",
|
|
31
|
+
"|",
|
|
32
|
+
"git push",
|
|
33
|
+
"git commit",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlanningPhase:
|
|
38
|
+
"""
|
|
39
|
+
Handles the planning phase for complex tasks.
|
|
40
|
+
|
|
41
|
+
In planning mode:
|
|
42
|
+
- Agent can only use read-only tools
|
|
43
|
+
- Agent explores the codebase to understand structure
|
|
44
|
+
- Agent creates an implementation plan
|
|
45
|
+
- User approves/modifies/rejects the plan
|
|
46
|
+
- After approval, execution proceeds with full tools
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, llm_client, tool_executor, agent_loop):
|
|
50
|
+
"""
|
|
51
|
+
Initialize planning phase.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
llm_client: LLM client instance
|
|
55
|
+
tool_executor: Tool executor instance
|
|
56
|
+
agent_loop: Agent loop instance for execution
|
|
57
|
+
"""
|
|
58
|
+
self.llm_client = llm_client
|
|
59
|
+
self.tool_executor = tool_executor
|
|
60
|
+
self.agent_loop = agent_loop
|
|
61
|
+
|
|
62
|
+
def execute_planning(self, user_message: str, base_messages: List[Dict]) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Execute the planning phase.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
user_message: The user's original request
|
|
68
|
+
base_messages: Base conversation messages
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The generated plan text
|
|
72
|
+
"""
|
|
73
|
+
console.print("\n[bold yellow]→ Entering PLANNING MODE[/bold yellow]")
|
|
74
|
+
console.print(
|
|
75
|
+
"[dim]Agent will explore the codebase with read-only tools and create a plan.[/dim]\n"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
planning_messages = base_messages.copy()
|
|
79
|
+
|
|
80
|
+
planning_messages.append(
|
|
81
|
+
{
|
|
82
|
+
"role": "system",
|
|
83
|
+
"content": """# PLANNING MODE
|
|
84
|
+
|
|
85
|
+
You are now in PLANNING MODE. Your task is to:
|
|
86
|
+
1. EXPLORE the codebase using read-only tools
|
|
87
|
+
2. DESIGN an implementation approach
|
|
88
|
+
3. CREATE a detailed plan
|
|
89
|
+
|
|
90
|
+
# Available Tools (READ-ONLY)
|
|
91
|
+
|
|
92
|
+
You can ONLY use these tools:
|
|
93
|
+
- read_file: Read file contents
|
|
94
|
+
- list_files: List files matching patterns (supports recursive patterns like "**/*.py")
|
|
95
|
+
- search_files: Search for text in files
|
|
96
|
+
- run_bash: Run ONLY read-only commands (ls, find, cat, grep, tree, etc.)
|
|
97
|
+
|
|
98
|
+
You CANNOT use:
|
|
99
|
+
- write_file
|
|
100
|
+
- edit_file
|
|
101
|
+
- create_directory
|
|
102
|
+
- Any destructive bash commands
|
|
103
|
+
|
|
104
|
+
# How to Explore the Codebase
|
|
105
|
+
|
|
106
|
+
**IMPORTANT**: To get a complete understanding, you MUST explore systematically:
|
|
107
|
+
|
|
108
|
+
1. **Start with directory structure**:
|
|
109
|
+
- Use `list_files` with pattern `**/*` to see ALL files and directories recursively
|
|
110
|
+
- Or use `run_bash` with command `find . -type f` to list all files
|
|
111
|
+
- Or use `run_bash` with command `tree` or `ls -R` to see the full structure
|
|
112
|
+
|
|
113
|
+
2. **List files by type**:
|
|
114
|
+
- Python: `list_files` with pattern `**/*.py`
|
|
115
|
+
- JavaScript: `list_files` with pattern `**/*.js`
|
|
116
|
+
- All code files: Try multiple patterns to cover all file types
|
|
117
|
+
|
|
118
|
+
3. **Read key files**:
|
|
119
|
+
- README files
|
|
120
|
+
- Configuration files (package.json, requirements.txt, pyproject.toml, etc.)
|
|
121
|
+
- Main entry points
|
|
122
|
+
- Important modules based on the task
|
|
123
|
+
|
|
124
|
+
4. **Search for specific patterns**:
|
|
125
|
+
- Use `search_files` to find classes, functions, imports, etc.
|
|
126
|
+
|
|
127
|
+
**Example exploration workflow**:
|
|
128
|
+
```tool_call
|
|
129
|
+
{
|
|
130
|
+
"tool": "list_files",
|
|
131
|
+
"parameters": {
|
|
132
|
+
"pattern": "**/*"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Then read relevant files:
|
|
138
|
+
```tool_call
|
|
139
|
+
{
|
|
140
|
+
"tool": "read_file",
|
|
141
|
+
"parameters": {
|
|
142
|
+
"file_path": "path/to/important/file.py"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
# Plan Format
|
|
148
|
+
|
|
149
|
+
When ready, create a plan in this format:
|
|
150
|
+
|
|
151
|
+
## Implementation Plan
|
|
152
|
+
|
|
153
|
+
### Overview
|
|
154
|
+
[Brief description of what you'll do]
|
|
155
|
+
|
|
156
|
+
### Steps
|
|
157
|
+
1. [First step]
|
|
158
|
+
2. [Second step]
|
|
159
|
+
3. [etc.]
|
|
160
|
+
|
|
161
|
+
### Files to Modify
|
|
162
|
+
- file1.py: [what changes]
|
|
163
|
+
- file2.py: [what changes]
|
|
164
|
+
|
|
165
|
+
### Risks
|
|
166
|
+
- [Potential issues or concerns]
|
|
167
|
+
|
|
168
|
+
# Completion
|
|
169
|
+
|
|
170
|
+
Say "PLAN_COMPLETE" when your plan is ready.""",
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
planning_messages.append(
|
|
175
|
+
{
|
|
176
|
+
"role": "user",
|
|
177
|
+
"content": (
|
|
178
|
+
f"Task: {user_message}\n\n"
|
|
179
|
+
"Please explore the codebase and create an implementation plan."
|
|
180
|
+
),
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
original_executor = self.agent_loop.tool_executor
|
|
185
|
+
restricted_executor = RestrictedToolExecutor(
|
|
186
|
+
self.tool_executor, PLANNING_ALLOWED_TOOLS
|
|
187
|
+
)
|
|
188
|
+
self.agent_loop.tool_executor = restricted_executor
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
old_max = self.agent_loop.max_iterations
|
|
192
|
+
self.agent_loop.max_iterations = 10
|
|
193
|
+
|
|
194
|
+
from .main import KubrickCLI
|
|
195
|
+
|
|
196
|
+
temp_cli = KubrickCLI.__new__(KubrickCLI)
|
|
197
|
+
tool_parser = temp_cli.parse_tool_calls
|
|
198
|
+
|
|
199
|
+
self.agent_loop.run(
|
|
200
|
+
messages=planning_messages,
|
|
201
|
+
tool_parser=tool_parser,
|
|
202
|
+
display_callback=None,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.agent_loop.max_iterations = old_max
|
|
206
|
+
|
|
207
|
+
plan_text = ""
|
|
208
|
+
for msg in reversed(planning_messages):
|
|
209
|
+
if msg["role"] == "assistant":
|
|
210
|
+
plan_text = msg["content"]
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
return plan_text
|
|
214
|
+
|
|
215
|
+
finally:
|
|
216
|
+
self.agent_loop.tool_executor = original_executor
|
|
217
|
+
|
|
218
|
+
def get_user_approval(self, plan: str) -> Dict:
|
|
219
|
+
"""
|
|
220
|
+
Present plan to user and get approval.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
plan: The generated plan text
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Dict with 'approved' (bool) and optional 'modifications' (str)
|
|
227
|
+
"""
|
|
228
|
+
console.print("\n" + "=" * 70)
|
|
229
|
+
console.print(
|
|
230
|
+
Panel(
|
|
231
|
+
Markdown(plan),
|
|
232
|
+
title="[bold cyan]Implementation Plan[/bold cyan]",
|
|
233
|
+
border_style="cyan",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
console.print("=" * 70 + "\n")
|
|
237
|
+
|
|
238
|
+
choice = Prompt.ask(
|
|
239
|
+
"[bold yellow]Approve this plan?[/bold yellow]",
|
|
240
|
+
choices=["approve", "modify", "reject"],
|
|
241
|
+
default="approve",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if choice == "approve":
|
|
245
|
+
console.print(
|
|
246
|
+
"[green]✓ Plan approved, proceeding with implementation[/green]"
|
|
247
|
+
)
|
|
248
|
+
return {"approved": True}
|
|
249
|
+
|
|
250
|
+
elif choice == "modify":
|
|
251
|
+
modifications = Prompt.ask(
|
|
252
|
+
"[yellow]What modifications would you like?[/yellow]"
|
|
253
|
+
)
|
|
254
|
+
console.print("[yellow]Plan modifications noted, will adjust[/yellow]")
|
|
255
|
+
return {"approved": True, "modifications": modifications}
|
|
256
|
+
|
|
257
|
+
else:
|
|
258
|
+
console.print("[red]Plan rejected, cancelling task[/red]")
|
|
259
|
+
return {"approved": False}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class RestrictedToolExecutor:
|
|
263
|
+
"""
|
|
264
|
+
Wraps ToolExecutor to restrict to read-only tools during planning.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def __init__(self, base_executor, allowed_tools: set):
|
|
268
|
+
"""
|
|
269
|
+
Initialize restricted executor.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
base_executor: Base ToolExecutor instance
|
|
273
|
+
allowed_tools: Set of allowed tool names
|
|
274
|
+
"""
|
|
275
|
+
self.base_executor = base_executor
|
|
276
|
+
self.allowed_tools = allowed_tools
|
|
277
|
+
|
|
278
|
+
def execute(self, tool_name: str, parameters: Dict) -> Dict:
|
|
279
|
+
"""
|
|
280
|
+
Execute tool with restrictions.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
tool_name: Name of tool to execute
|
|
284
|
+
parameters: Tool parameters
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Result dict
|
|
288
|
+
"""
|
|
289
|
+
if tool_name not in self.allowed_tools:
|
|
290
|
+
return {
|
|
291
|
+
"success": False,
|
|
292
|
+
"error": f"Tool '{tool_name}' is not allowed in planning mode (read-only)",
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if tool_name == "run_bash":
|
|
296
|
+
command = parameters.get("command", "")
|
|
297
|
+
if self._is_dangerous_command(command):
|
|
298
|
+
return {
|
|
299
|
+
"success": False,
|
|
300
|
+
"error": (
|
|
301
|
+
f"Bash command '{command}' is not allowed in "
|
|
302
|
+
"planning mode (read-only)"
|
|
303
|
+
),
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return self.base_executor.execute(tool_name, parameters)
|
|
307
|
+
|
|
308
|
+
def _is_dangerous_command(self, command: str) -> bool:
|
|
309
|
+
"""Check if bash command is dangerous."""
|
|
310
|
+
command_lower = command.lower()
|
|
311
|
+
for pattern in DANGEROUS_BASH_PATTERNS:
|
|
312
|
+
if pattern in command_lower:
|
|
313
|
+
return True
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def working_dir(self):
|
|
318
|
+
"""Pass through working_dir property."""
|
|
319
|
+
return self.base_executor.working_dir
|
kubrick_cli/progress.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Progress tracking and visualization for multi-step tasks."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.progress import (
|
|
7
|
+
BarColumn,
|
|
8
|
+
Progress,
|
|
9
|
+
SpinnerColumn,
|
|
10
|
+
TaskID,
|
|
11
|
+
TextColumn,
|
|
12
|
+
TimeElapsedColumn,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProgressTracker:
|
|
19
|
+
"""
|
|
20
|
+
Tracks and displays progress for multi-step agentic tasks.
|
|
21
|
+
|
|
22
|
+
Provides visual feedback during long-running operations.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, enabled: bool = True):
|
|
26
|
+
"""
|
|
27
|
+
Initialize progress tracker.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
enabled: Whether progress tracking is enabled
|
|
31
|
+
"""
|
|
32
|
+
self.enabled = enabled
|
|
33
|
+
self.progress: Optional[Progress] = None
|
|
34
|
+
self.current_task: Optional[TaskID] = None
|
|
35
|
+
self.step_count = 0
|
|
36
|
+
self.total_steps = 0
|
|
37
|
+
|
|
38
|
+
def start(self, total_steps: int = None, description: str = "Working"):
|
|
39
|
+
"""
|
|
40
|
+
Start progress tracking.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
total_steps: Total number of steps (if known)
|
|
44
|
+
description: Initial description
|
|
45
|
+
"""
|
|
46
|
+
if not self.enabled:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
self.total_steps = total_steps or 0
|
|
50
|
+
self.step_count = 0
|
|
51
|
+
|
|
52
|
+
if total_steps:
|
|
53
|
+
self.progress = Progress(
|
|
54
|
+
SpinnerColumn(),
|
|
55
|
+
TextColumn("[bold cyan]{task.description}"),
|
|
56
|
+
BarColumn(),
|
|
57
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
58
|
+
TimeElapsedColumn(),
|
|
59
|
+
console=console,
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
self.progress = Progress(
|
|
63
|
+
SpinnerColumn(),
|
|
64
|
+
TextColumn("[bold cyan]{task.description}"),
|
|
65
|
+
TimeElapsedColumn(),
|
|
66
|
+
console=console,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self.progress.start()
|
|
70
|
+
self.current_task = self.progress.add_task(description, total=total_steps)
|
|
71
|
+
|
|
72
|
+
def update(self, description: str = None, advance: int = 1, completed: int = None):
|
|
73
|
+
"""
|
|
74
|
+
Update progress.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
description: New description (optional)
|
|
78
|
+
advance: Amount to advance (default: 1)
|
|
79
|
+
completed: Set absolute completion (optional)
|
|
80
|
+
"""
|
|
81
|
+
if not self.enabled or not self.progress:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if completed is not None:
|
|
85
|
+
self.step_count = completed
|
|
86
|
+
self.progress.update(self.current_task, completed=completed)
|
|
87
|
+
else:
|
|
88
|
+
self.step_count += advance
|
|
89
|
+
self.progress.update(self.current_task, advance=advance)
|
|
90
|
+
|
|
91
|
+
if description:
|
|
92
|
+
self.progress.update(self.current_task, description=description)
|
|
93
|
+
|
|
94
|
+
def update_description(self, description: str):
|
|
95
|
+
"""
|
|
96
|
+
Update just the description.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
description: New description
|
|
100
|
+
"""
|
|
101
|
+
if not self.enabled or not self.progress:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
self.progress.update(self.current_task, description=description)
|
|
105
|
+
|
|
106
|
+
def step(self, description: str):
|
|
107
|
+
"""
|
|
108
|
+
Mark a step complete with new description.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
description: Description of the new step
|
|
112
|
+
"""
|
|
113
|
+
if not self.enabled or not self.progress:
|
|
114
|
+
console.print(f"[dim]→ {description}[/dim]")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
self.step_count += 1
|
|
118
|
+
step_info = ""
|
|
119
|
+
if self.total_steps > 0:
|
|
120
|
+
step_info = f"[Step {self.step_count}/{self.total_steps}] "
|
|
121
|
+
|
|
122
|
+
self.progress.update(
|
|
123
|
+
self.current_task,
|
|
124
|
+
description=f"{step_info}{description}",
|
|
125
|
+
advance=1,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def complete(self, message: str = "Complete"):
|
|
129
|
+
"""
|
|
130
|
+
Mark progress as complete.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
message: Completion message
|
|
134
|
+
"""
|
|
135
|
+
if not self.enabled or not self.progress:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if self.total_steps:
|
|
139
|
+
self.progress.update(self.current_task, completed=self.total_steps)
|
|
140
|
+
|
|
141
|
+
self.progress.update(self.current_task, description=message)
|
|
142
|
+
self.progress.stop()
|
|
143
|
+
self.progress = None
|
|
144
|
+
self.current_task = None
|
|
145
|
+
|
|
146
|
+
def stop(self):
|
|
147
|
+
"""Stop progress tracking."""
|
|
148
|
+
if not self.enabled or not self.progress:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
self.progress.stop()
|
|
152
|
+
self.progress = None
|
|
153
|
+
self.current_task = None
|
|
154
|
+
|
|
155
|
+
def __enter__(self):
|
|
156
|
+
"""Context manager entry."""
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
160
|
+
"""Context manager exit."""
|
|
161
|
+
if self.progress:
|
|
162
|
+
self.stop()
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Anthropic provider adapter."""
|
|
2
|
+
|
|
3
|
+
import http.client
|
|
4
|
+
import json
|
|
5
|
+
import ssl
|
|
6
|
+
from typing import Dict, Iterator, List
|
|
7
|
+
|
|
8
|
+
from .base import ProviderAdapter, ProviderMetadata
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AnthropicProvider(ProviderAdapter):
|
|
12
|
+
"""Provider adapter for Anthropic API."""
|
|
13
|
+
|
|
14
|
+
METADATA = ProviderMetadata(
|
|
15
|
+
name="anthropic",
|
|
16
|
+
display_name="Anthropic",
|
|
17
|
+
description="Anthropic API (Claude Sonnet 4.5, etc.)",
|
|
18
|
+
config_fields=[
|
|
19
|
+
{
|
|
20
|
+
"key": "anthropic_api_key",
|
|
21
|
+
"label": "Anthropic API key",
|
|
22
|
+
"type": "password",
|
|
23
|
+
"help_text": "Get your API key from: https://console.anthropic.com/settings/keys",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"key": "anthropic_model",
|
|
27
|
+
"label": "Model name",
|
|
28
|
+
"type": "text",
|
|
29
|
+
"default": "claude-sonnet-4-5-20250929",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
anthropic_api_key: str,
|
|
37
|
+
anthropic_model: str = "claude-sonnet-4-5-20250929",
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Initialize Anthropic provider.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
anthropic_api_key: Anthropic API key
|
|
44
|
+
anthropic_model: Model name (default: claude-3-5-sonnet-20241022)
|
|
45
|
+
"""
|
|
46
|
+
self.api_key = anthropic_api_key
|
|
47
|
+
self._model_name = anthropic_model
|
|
48
|
+
self.base_url = "api.anthropic.com"
|
|
49
|
+
self.timeout = 600
|
|
50
|
+
self.api_version = "2023-06-01"
|
|
51
|
+
|
|
52
|
+
def generate_streaming(
|
|
53
|
+
self, messages: List[Dict[str, str]], stream_options: Dict = None
|
|
54
|
+
) -> Iterator[str]:
|
|
55
|
+
"""
|
|
56
|
+
Generate streaming response from Anthropic.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
messages: List of message dicts with 'role' and 'content'
|
|
60
|
+
stream_options: Optional streaming parameters
|
|
61
|
+
|
|
62
|
+
Yields:
|
|
63
|
+
Text chunks as they arrive
|
|
64
|
+
"""
|
|
65
|
+
system_message = ""
|
|
66
|
+
conversation_messages = []
|
|
67
|
+
|
|
68
|
+
for msg in messages:
|
|
69
|
+
if msg["role"] == "system":
|
|
70
|
+
system_message = msg["content"]
|
|
71
|
+
else:
|
|
72
|
+
conversation_messages.append(msg)
|
|
73
|
+
|
|
74
|
+
payload = {
|
|
75
|
+
"model": self._model_name,
|
|
76
|
+
"messages": conversation_messages,
|
|
77
|
+
"max_tokens": 4096,
|
|
78
|
+
"stream": True,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if system_message:
|
|
82
|
+
payload["system"] = system_message
|
|
83
|
+
|
|
84
|
+
if stream_options:
|
|
85
|
+
if "temperature" in stream_options:
|
|
86
|
+
payload["temperature"] = stream_options["temperature"]
|
|
87
|
+
if "max_tokens" in stream_options:
|
|
88
|
+
payload["max_tokens"] = stream_options["max_tokens"]
|
|
89
|
+
|
|
90
|
+
headers = {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"x-api-key": self.api_key,
|
|
93
|
+
"anthropic-version": self.api_version,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
body = json.dumps(payload).encode("utf-8")
|
|
97
|
+
|
|
98
|
+
context = ssl.create_default_context()
|
|
99
|
+
conn = http.client.HTTPSConnection(
|
|
100
|
+
self.base_url, 443, timeout=self.timeout, context=context
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
conn.request("POST", "/v1/messages", body=body, headers=headers)
|
|
105
|
+
response = conn.getresponse()
|
|
106
|
+
|
|
107
|
+
if response.status != 200:
|
|
108
|
+
error_body = response.read().decode("utf-8")
|
|
109
|
+
raise Exception(f"Anthropic API error {response.status}: {error_body}")
|
|
110
|
+
|
|
111
|
+
buffer = ""
|
|
112
|
+
while True:
|
|
113
|
+
chunk = response.read(1024)
|
|
114
|
+
if not chunk:
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
if isinstance(chunk, bytes):
|
|
118
|
+
chunk = chunk.decode("utf-8")
|
|
119
|
+
|
|
120
|
+
buffer += chunk
|
|
121
|
+
|
|
122
|
+
while "\n" in buffer:
|
|
123
|
+
line, buffer = buffer.split("\n", 1)
|
|
124
|
+
line = line.strip()
|
|
125
|
+
|
|
126
|
+
if not line:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if line.startswith("data: "):
|
|
130
|
+
line = line[6:]
|
|
131
|
+
|
|
132
|
+
if line.startswith("event: "):
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
data = json.loads(line)
|
|
137
|
+
|
|
138
|
+
event_type = data.get("type")
|
|
139
|
+
|
|
140
|
+
if event_type == "content_block_delta":
|
|
141
|
+
delta = data.get("delta", {})
|
|
142
|
+
if delta.get("type") == "text_delta":
|
|
143
|
+
text = delta.get("text", "")
|
|
144
|
+
if text:
|
|
145
|
+
yield text
|
|
146
|
+
|
|
147
|
+
elif event_type == "message_stop":
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
except json.JSONDecodeError:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
finally:
|
|
154
|
+
conn.close()
|
|
155
|
+
|
|
156
|
+
def generate(
|
|
157
|
+
self, messages: List[Dict[str, str]], stream_options: Dict = None
|
|
158
|
+
) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Generate non-streaming response from Anthropic.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
messages: List of message dicts with 'role' and 'content'
|
|
164
|
+
stream_options: Optional parameters
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Complete response text
|
|
168
|
+
"""
|
|
169
|
+
chunks = []
|
|
170
|
+
for chunk in self.generate_streaming(messages, stream_options):
|
|
171
|
+
chunks.append(chunk)
|
|
172
|
+
return "".join(chunks)
|
|
173
|
+
|
|
174
|
+
def is_healthy(self) -> bool:
|
|
175
|
+
"""
|
|
176
|
+
Check if Anthropic API is accessible.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
True if healthy, False otherwise
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
context = ssl.create_default_context()
|
|
183
|
+
conn = http.client.HTTPSConnection(
|
|
184
|
+
self.base_url, 443, timeout=10, context=context
|
|
185
|
+
)
|
|
186
|
+
headers = {
|
|
187
|
+
"x-api-key": self.api_key,
|
|
188
|
+
"anthropic-version": self.api_version,
|
|
189
|
+
}
|
|
190
|
+
conn.request("GET", "/v1/models", headers=headers)
|
|
191
|
+
response = conn.getresponse()
|
|
192
|
+
conn.close()
|
|
193
|
+
return response.status in (200, 404)
|
|
194
|
+
except Exception:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def provider_name(self) -> str:
|
|
199
|
+
"""Get provider name."""
|
|
200
|
+
return "anthropic"
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def model_name(self) -> str:
|
|
204
|
+
"""Get model name."""
|
|
205
|
+
return self._model_name
|
|
206
|
+
|
|
207
|
+
def set_model(self, model_name: str):
|
|
208
|
+
"""Set model name dynamically."""
|
|
209
|
+
self._model_name = model_name
|