codeframe-ai 0.9.0__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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Anthropic LLM provider implementation.
|
|
2
|
+
|
|
3
|
+
Provides Claude model access via the Anthropic API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
from typing import TYPE_CHECKING, AsyncIterator, Iterator, Optional
|
|
9
|
+
|
|
10
|
+
from codeframe.adapters.llm.base import (
|
|
11
|
+
LLMProvider,
|
|
12
|
+
LLMResponse,
|
|
13
|
+
ModelSelector,
|
|
14
|
+
Purpose,
|
|
15
|
+
StreamChunk,
|
|
16
|
+
Tool,
|
|
17
|
+
ToolCall,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from codeframe.core.credentials import CredentialManager
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AnthropicProvider(LLMProvider):
|
|
25
|
+
"""Anthropic Claude provider.
|
|
26
|
+
|
|
27
|
+
Uses the Anthropic Python SDK to make API calls.
|
|
28
|
+
Supports tool use and streaming.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
api_key: Optional[str] = None,
|
|
34
|
+
model_selector: Optional[ModelSelector] = None,
|
|
35
|
+
credential_manager: Optional["CredentialManager"] = None,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize the Anthropic provider.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
api_key: Anthropic API key (defaults to ANTHROPIC_API_KEY env var)
|
|
41
|
+
model_selector: Custom model selector
|
|
42
|
+
credential_manager: Optional credential manager for secure key retrieval
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If no API key is available
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(model_selector)
|
|
48
|
+
|
|
49
|
+
# Try to get API key from multiple sources in order:
|
|
50
|
+
# 1. Direct api_key parameter
|
|
51
|
+
# 2. CredentialManager (if provided)
|
|
52
|
+
# 3. Environment variable
|
|
53
|
+
self.api_key = api_key
|
|
54
|
+
|
|
55
|
+
if not self.api_key and credential_manager:
|
|
56
|
+
from codeframe.core.credentials import CredentialProvider
|
|
57
|
+
self.api_key = credential_manager.get_credential(CredentialProvider.LLM_ANTHROPIC)
|
|
58
|
+
|
|
59
|
+
if not self.api_key:
|
|
60
|
+
self.api_key = os.getenv("ANTHROPIC_API_KEY")
|
|
61
|
+
|
|
62
|
+
if not self.api_key:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"ANTHROPIC_API_KEY not set. "
|
|
65
|
+
"Set the environment variable, pass api_key parameter, "
|
|
66
|
+
"or configure via 'codeframe auth setup --provider anthropic'."
|
|
67
|
+
)
|
|
68
|
+
self._client = None
|
|
69
|
+
self._async_client = None
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def client(self):
|
|
73
|
+
"""Lazy-load the Anthropic client."""
|
|
74
|
+
if self._client is None:
|
|
75
|
+
from anthropic import Anthropic
|
|
76
|
+
|
|
77
|
+
self._client = Anthropic(api_key=self.api_key)
|
|
78
|
+
return self._client
|
|
79
|
+
|
|
80
|
+
def complete(
|
|
81
|
+
self,
|
|
82
|
+
messages: list[dict],
|
|
83
|
+
purpose: Purpose = Purpose.EXECUTION,
|
|
84
|
+
tools: Optional[list[Tool]] = None,
|
|
85
|
+
max_tokens: int = 4096,
|
|
86
|
+
temperature: float = 0.0,
|
|
87
|
+
system: Optional[str] = None,
|
|
88
|
+
) -> LLMResponse:
|
|
89
|
+
"""Generate a completion using Claude.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
messages: Conversation messages
|
|
93
|
+
purpose: Purpose of call (for model selection)
|
|
94
|
+
tools: Available tools for the model to use
|
|
95
|
+
max_tokens: Maximum tokens to generate
|
|
96
|
+
temperature: Sampling temperature
|
|
97
|
+
system: System prompt
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
LLMResponse with content and/or tool calls
|
|
101
|
+
"""
|
|
102
|
+
model = self.get_model(purpose)
|
|
103
|
+
|
|
104
|
+
# Build request kwargs
|
|
105
|
+
kwargs = {
|
|
106
|
+
"model": model,
|
|
107
|
+
"max_tokens": max_tokens,
|
|
108
|
+
"messages": self._convert_messages(messages),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if temperature > 0:
|
|
112
|
+
kwargs["temperature"] = temperature
|
|
113
|
+
|
|
114
|
+
if system:
|
|
115
|
+
kwargs["system"] = system
|
|
116
|
+
|
|
117
|
+
if tools:
|
|
118
|
+
kwargs["tools"] = self._convert_tools(tools)
|
|
119
|
+
|
|
120
|
+
# Make the API call
|
|
121
|
+
response = self.client.messages.create(**kwargs)
|
|
122
|
+
|
|
123
|
+
# Parse response
|
|
124
|
+
return self._parse_response(response)
|
|
125
|
+
|
|
126
|
+
async def async_complete(
|
|
127
|
+
self,
|
|
128
|
+
messages: list[dict],
|
|
129
|
+
purpose: Purpose = Purpose.EXECUTION,
|
|
130
|
+
tools: Optional[list[Tool]] = None,
|
|
131
|
+
max_tokens: int = 4096,
|
|
132
|
+
temperature: float = 0.0,
|
|
133
|
+
system: Optional[str] = None,
|
|
134
|
+
) -> LLMResponse:
|
|
135
|
+
"""True async completion via AsyncAnthropic.
|
|
136
|
+
|
|
137
|
+
Raises LLMAuthError / LLMRateLimitError / LLMConnectionError on failure.
|
|
138
|
+
"""
|
|
139
|
+
from anthropic import AsyncAnthropic
|
|
140
|
+
from anthropic import (
|
|
141
|
+
AuthenticationError,
|
|
142
|
+
RateLimitError,
|
|
143
|
+
APIConnectionError,
|
|
144
|
+
)
|
|
145
|
+
from codeframe.adapters.llm.base import (
|
|
146
|
+
LLMAuthError,
|
|
147
|
+
LLMRateLimitError,
|
|
148
|
+
LLMConnectionError,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if self._async_client is None:
|
|
152
|
+
self._async_client = AsyncAnthropic(api_key=self.api_key)
|
|
153
|
+
|
|
154
|
+
model = self.get_model(purpose)
|
|
155
|
+
kwargs: dict = {
|
|
156
|
+
"model": model,
|
|
157
|
+
"max_tokens": max_tokens,
|
|
158
|
+
"messages": self._convert_messages(messages),
|
|
159
|
+
}
|
|
160
|
+
if temperature > 0:
|
|
161
|
+
kwargs["temperature"] = temperature
|
|
162
|
+
if system:
|
|
163
|
+
kwargs["system"] = system
|
|
164
|
+
if tools:
|
|
165
|
+
kwargs["tools"] = self._convert_tools(tools)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
response = await self._async_client.messages.create(**kwargs)
|
|
169
|
+
return self._parse_response(response)
|
|
170
|
+
except AuthenticationError as exc:
|
|
171
|
+
raise LLMAuthError(str(exc)) from exc
|
|
172
|
+
except RateLimitError as exc:
|
|
173
|
+
raise LLMRateLimitError(str(exc)) from exc
|
|
174
|
+
except APIConnectionError as exc:
|
|
175
|
+
raise LLMConnectionError(str(exc)) from exc
|
|
176
|
+
|
|
177
|
+
def supports(self, capability: str) -> bool:
|
|
178
|
+
"""Return True for capabilities this provider supports."""
|
|
179
|
+
return capability == "extended_thinking"
|
|
180
|
+
|
|
181
|
+
async def async_stream(
|
|
182
|
+
self,
|
|
183
|
+
messages: list[dict],
|
|
184
|
+
system: str,
|
|
185
|
+
tools: list[dict],
|
|
186
|
+
model: str,
|
|
187
|
+
max_tokens: int,
|
|
188
|
+
interrupt_event: Optional[asyncio.Event] = None,
|
|
189
|
+
extended_thinking: bool = False,
|
|
190
|
+
) -> AsyncIterator[StreamChunk]:
|
|
191
|
+
"""Stream using Anthropic AsyncAnthropic SDK, yielding StreamChunk objects.
|
|
192
|
+
|
|
193
|
+
Translates Anthropic SDK events into the normalized StreamChunk format.
|
|
194
|
+
Tool inputs are collected and emitted in the final message_stop chunk
|
|
195
|
+
via tool_inputs_by_id, which is more reliable than streaming input deltas.
|
|
196
|
+
|
|
197
|
+
When ``extended_thinking=True``, requests interleaved thinking via the
|
|
198
|
+
Anthropic betas API. The flag is silently ignored on SDK versions that
|
|
199
|
+
do not support it.
|
|
200
|
+
"""
|
|
201
|
+
from anthropic import AsyncAnthropic
|
|
202
|
+
|
|
203
|
+
if self._async_client is None:
|
|
204
|
+
self._async_client = AsyncAnthropic(api_key=self.api_key)
|
|
205
|
+
|
|
206
|
+
# Convert messages to Anthropic API format (handles tool_calls/tool_results)
|
|
207
|
+
converted = self._convert_messages(messages)
|
|
208
|
+
|
|
209
|
+
kwargs: dict = {
|
|
210
|
+
"model": model,
|
|
211
|
+
"system": system,
|
|
212
|
+
"messages": converted,
|
|
213
|
+
"tools": tools,
|
|
214
|
+
"max_tokens": max_tokens,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if extended_thinking:
|
|
218
|
+
kwargs["betas"] = ["interleaved-thinking-2025-05-14"]
|
|
219
|
+
|
|
220
|
+
active_tool_id: Optional[str] = None
|
|
221
|
+
|
|
222
|
+
# When extended_thinking is set, the beta header may be unsupported on
|
|
223
|
+
# older SDK versions. Retry without it rather than hard-failing.
|
|
224
|
+
try:
|
|
225
|
+
stream_ctx = self._async_client.messages.stream(**kwargs)
|
|
226
|
+
except Exception: # pragma: no cover
|
|
227
|
+
if extended_thinking:
|
|
228
|
+
kwargs.pop("betas", None)
|
|
229
|
+
stream_ctx = self._async_client.messages.stream(**kwargs)
|
|
230
|
+
else:
|
|
231
|
+
raise
|
|
232
|
+
|
|
233
|
+
async with stream_ctx as stream:
|
|
234
|
+
async for sdk_event in stream:
|
|
235
|
+
if interrupt_event and interrupt_event.is_set():
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
event_type = sdk_event.type
|
|
239
|
+
|
|
240
|
+
if event_type == "content_block_start":
|
|
241
|
+
block = sdk_event.content_block
|
|
242
|
+
if block.type == "tool_use":
|
|
243
|
+
active_tool_id = block.id
|
|
244
|
+
yield StreamChunk(
|
|
245
|
+
type="tool_use_start",
|
|
246
|
+
tool_id=block.id,
|
|
247
|
+
tool_name=block.name,
|
|
248
|
+
tool_input=getattr(block, "input", {}),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
elif event_type == "content_block_delta":
|
|
252
|
+
delta = sdk_event.delta
|
|
253
|
+
if delta.type == "text_delta":
|
|
254
|
+
yield StreamChunk(type="text_delta", text=delta.text)
|
|
255
|
+
elif delta.type == "thinking_delta":
|
|
256
|
+
yield StreamChunk(type="thinking_delta", text=delta.thinking)
|
|
257
|
+
# input_json_delta: final inputs are rebuilt from message_stop
|
|
258
|
+
|
|
259
|
+
elif event_type == "content_block_stop":
|
|
260
|
+
if active_tool_id is not None:
|
|
261
|
+
yield StreamChunk(type="tool_use_stop")
|
|
262
|
+
active_tool_id = None
|
|
263
|
+
|
|
264
|
+
elif event_type == "message_stop":
|
|
265
|
+
# Flush any open tool block
|
|
266
|
+
if active_tool_id is not None:
|
|
267
|
+
yield StreamChunk(type="tool_use_stop")
|
|
268
|
+
active_tool_id = None
|
|
269
|
+
|
|
270
|
+
final_msg = await stream.get_final_message()
|
|
271
|
+
stop_reason = final_msg.stop_reason or "end_turn"
|
|
272
|
+
|
|
273
|
+
# Build tool_inputs_by_id from final content blocks
|
|
274
|
+
tool_inputs_by_id: dict = {}
|
|
275
|
+
if hasattr(final_msg, "content"):
|
|
276
|
+
for block in final_msg.content:
|
|
277
|
+
if getattr(block, "type", None) == "tool_use" and hasattr(block, "id"):
|
|
278
|
+
tool_inputs_by_id[block.id] = getattr(block, "input", {})
|
|
279
|
+
|
|
280
|
+
yield StreamChunk(
|
|
281
|
+
type="message_stop",
|
|
282
|
+
stop_reason=stop_reason,
|
|
283
|
+
input_tokens=final_msg.usage.input_tokens,
|
|
284
|
+
output_tokens=final_msg.usage.output_tokens,
|
|
285
|
+
tool_inputs_by_id=tool_inputs_by_id,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def stream(
|
|
289
|
+
self,
|
|
290
|
+
messages: list[dict],
|
|
291
|
+
purpose: Purpose = Purpose.EXECUTION,
|
|
292
|
+
max_tokens: int = 4096,
|
|
293
|
+
temperature: float = 0.0,
|
|
294
|
+
system: Optional[str] = None,
|
|
295
|
+
) -> Iterator[str]:
|
|
296
|
+
"""Stream a completion token by token.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
messages: Conversation messages
|
|
300
|
+
purpose: Purpose of call (for model selection)
|
|
301
|
+
max_tokens: Maximum tokens to generate
|
|
302
|
+
temperature: Sampling temperature
|
|
303
|
+
system: System prompt
|
|
304
|
+
|
|
305
|
+
Yields:
|
|
306
|
+
Text chunks as they are generated
|
|
307
|
+
"""
|
|
308
|
+
model = self.get_model(purpose)
|
|
309
|
+
|
|
310
|
+
kwargs = {
|
|
311
|
+
"model": model,
|
|
312
|
+
"max_tokens": max_tokens,
|
|
313
|
+
"messages": self._convert_messages(messages),
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if temperature > 0:
|
|
317
|
+
kwargs["temperature"] = temperature
|
|
318
|
+
|
|
319
|
+
if system:
|
|
320
|
+
kwargs["system"] = system
|
|
321
|
+
|
|
322
|
+
with self.client.messages.stream(**kwargs) as stream:
|
|
323
|
+
for text in stream.text_stream:
|
|
324
|
+
yield text
|
|
325
|
+
|
|
326
|
+
def _convert_messages(self, messages: list[dict]) -> list[dict]:
|
|
327
|
+
"""Convert messages to Anthropic format.
|
|
328
|
+
|
|
329
|
+
Handles tool results by converting them to the expected format.
|
|
330
|
+
"""
|
|
331
|
+
converted = []
|
|
332
|
+
for msg in messages:
|
|
333
|
+
if "tool_results" in msg and msg["tool_results"]:
|
|
334
|
+
# Convert tool results to Anthropic format
|
|
335
|
+
# Mirror tool_calls logic: tool_result blocks first, then text if present
|
|
336
|
+
content = []
|
|
337
|
+
for tr in msg["tool_results"]:
|
|
338
|
+
content.append(
|
|
339
|
+
{
|
|
340
|
+
"type": "tool_result",
|
|
341
|
+
"tool_use_id": tr["tool_call_id"],
|
|
342
|
+
"content": tr["content"],
|
|
343
|
+
"is_error": tr.get("is_error", False),
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
# Preserve any user text content alongside tool results
|
|
347
|
+
if msg.get("content"):
|
|
348
|
+
msg_content = msg["content"]
|
|
349
|
+
if isinstance(msg_content, str):
|
|
350
|
+
content.append({"type": "text", "text": msg_content})
|
|
351
|
+
elif isinstance(msg_content, list):
|
|
352
|
+
# Handle list of content blocks
|
|
353
|
+
for block in msg_content:
|
|
354
|
+
if isinstance(block, str):
|
|
355
|
+
content.append({"type": "text", "text": block})
|
|
356
|
+
elif isinstance(block, dict) and block.get("type") == "text":
|
|
357
|
+
content.append(block)
|
|
358
|
+
converted.append({"role": "user", "content": content})
|
|
359
|
+
elif "tool_calls" in msg and msg["tool_calls"]:
|
|
360
|
+
# Convert assistant message with tool calls
|
|
361
|
+
content = []
|
|
362
|
+
if msg.get("content"):
|
|
363
|
+
content.append({"type": "text", "text": msg["content"]})
|
|
364
|
+
for tc in msg["tool_calls"]:
|
|
365
|
+
content.append(
|
|
366
|
+
{
|
|
367
|
+
"type": "tool_use",
|
|
368
|
+
"id": tc["id"],
|
|
369
|
+
"name": tc["name"],
|
|
370
|
+
"input": tc["input"],
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
converted.append({"role": "assistant", "content": content})
|
|
374
|
+
else:
|
|
375
|
+
# Simple text message
|
|
376
|
+
converted.append({"role": msg["role"], "content": msg["content"]})
|
|
377
|
+
return converted
|
|
378
|
+
|
|
379
|
+
def _convert_tools(self, tools: list[Tool]) -> list[dict]:
|
|
380
|
+
"""Convert Tool objects to Anthropic format."""
|
|
381
|
+
return [
|
|
382
|
+
{
|
|
383
|
+
"name": tool.name,
|
|
384
|
+
"description": tool.description,
|
|
385
|
+
"input_schema": tool.input_schema,
|
|
386
|
+
}
|
|
387
|
+
for tool in tools
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
def _parse_response(self, response) -> LLMResponse:
|
|
391
|
+
"""Parse Anthropic response into LLMResponse."""
|
|
392
|
+
content = ""
|
|
393
|
+
tool_calls = []
|
|
394
|
+
|
|
395
|
+
for block in response.content:
|
|
396
|
+
if block.type == "text":
|
|
397
|
+
content += block.text
|
|
398
|
+
elif block.type == "tool_use":
|
|
399
|
+
tool_calls.append(
|
|
400
|
+
ToolCall(
|
|
401
|
+
id=block.id,
|
|
402
|
+
name=block.name,
|
|
403
|
+
input=block.input,
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
return LLMResponse(
|
|
408
|
+
content=content,
|
|
409
|
+
tool_calls=tool_calls,
|
|
410
|
+
stop_reason=response.stop_reason,
|
|
411
|
+
model=response.model,
|
|
412
|
+
input_tokens=response.usage.input_tokens,
|
|
413
|
+
output_tokens=response.usage.output_tokens,
|
|
414
|
+
)
|