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,444 @@
|
|
|
1
|
+
"""Base LLM adapter interface.
|
|
2
|
+
|
|
3
|
+
Defines the protocol that all LLM providers must implement,
|
|
4
|
+
along with shared data structures for requests and responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import AsyncIterator, Iterator, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Common exception hierarchy
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LLMError(Exception):
|
|
21
|
+
"""Base exception for LLM provider errors."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LLMAuthError(LLMError):
|
|
25
|
+
"""Authentication failure (bad key, expired token, etc.)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LLMRateLimitError(LLMError):
|
|
29
|
+
"""Rate limit exceeded — caller may retry after a backoff."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LLMConnectionError(LLMError):
|
|
33
|
+
"""Network or connection error."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Purpose(str, Enum):
|
|
37
|
+
"""Purpose of an LLM call, used for model selection."""
|
|
38
|
+
|
|
39
|
+
PLANNING = "planning" # Complex reasoning, architecture decisions
|
|
40
|
+
EXECUTION = "execution" # Code generation, editing
|
|
41
|
+
GENERATION = "generation" # Simple text generation, summaries
|
|
42
|
+
CORRECTION = "correction" # Self-correction after errors (uses stronger model)
|
|
43
|
+
SUPERVISION = "supervision" # Supervisor decisions (blocker triage, tactical resolution)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Default model aliases (use valid Anthropic model identifiers)
|
|
47
|
+
DEFAULT_PLANNING_MODEL = "claude-sonnet-4-20250514"
|
|
48
|
+
DEFAULT_EXECUTION_MODEL = "claude-sonnet-4-20250514"
|
|
49
|
+
DEFAULT_GENERATION_MODEL = "claude-3-5-haiku-20241022"
|
|
50
|
+
DEFAULT_CORRECTION_MODEL = "claude-opus-4-20250514" # Step up for fixing errors
|
|
51
|
+
DEFAULT_SUPERVISION_MODEL = "claude-opus-4-20250514" # Strong model for supervisor decisions
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ModelSelector:
|
|
56
|
+
"""Task-based model selection heuristics.
|
|
57
|
+
|
|
58
|
+
Maps operation purposes to appropriate models. Model names can be
|
|
59
|
+
overridden via environment variables:
|
|
60
|
+
- CODEFRAME_PLANNING_MODEL
|
|
61
|
+
- CODEFRAME_EXECUTION_MODEL
|
|
62
|
+
- CODEFRAME_GENERATION_MODEL
|
|
63
|
+
- CODEFRAME_CORRECTION_MODEL
|
|
64
|
+
- CODEFRAME_SUPERVISION_MODEL
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
planning_model: str = ""
|
|
68
|
+
execution_model: str = ""
|
|
69
|
+
generation_model: str = ""
|
|
70
|
+
correction_model: str = ""
|
|
71
|
+
supervision_model: str = ""
|
|
72
|
+
|
|
73
|
+
def __post_init__(self):
|
|
74
|
+
"""Load model names from environment or use defaults.
|
|
75
|
+
|
|
76
|
+
Only assigns from environment/defaults when the field is empty/falsy,
|
|
77
|
+
respecting any explicit constructor overrides.
|
|
78
|
+
"""
|
|
79
|
+
if not self.planning_model:
|
|
80
|
+
self.planning_model = os.getenv(
|
|
81
|
+
"CODEFRAME_PLANNING_MODEL", DEFAULT_PLANNING_MODEL
|
|
82
|
+
)
|
|
83
|
+
if not self.execution_model:
|
|
84
|
+
self.execution_model = os.getenv(
|
|
85
|
+
"CODEFRAME_EXECUTION_MODEL", DEFAULT_EXECUTION_MODEL
|
|
86
|
+
)
|
|
87
|
+
if not self.generation_model:
|
|
88
|
+
self.generation_model = os.getenv(
|
|
89
|
+
"CODEFRAME_GENERATION_MODEL", DEFAULT_GENERATION_MODEL
|
|
90
|
+
)
|
|
91
|
+
if not self.correction_model:
|
|
92
|
+
self.correction_model = os.getenv(
|
|
93
|
+
"CODEFRAME_CORRECTION_MODEL", DEFAULT_CORRECTION_MODEL
|
|
94
|
+
)
|
|
95
|
+
if not self.supervision_model:
|
|
96
|
+
self.supervision_model = os.getenv(
|
|
97
|
+
"CODEFRAME_SUPERVISION_MODEL", DEFAULT_SUPERVISION_MODEL
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def for_purpose(self, purpose: Purpose) -> str:
|
|
101
|
+
"""Get the model for a given purpose.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
purpose: The purpose of the LLM call
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Model identifier string
|
|
108
|
+
"""
|
|
109
|
+
if purpose == Purpose.PLANNING:
|
|
110
|
+
return self.planning_model
|
|
111
|
+
elif purpose == Purpose.EXECUTION:
|
|
112
|
+
return self.execution_model
|
|
113
|
+
elif purpose == Purpose.GENERATION:
|
|
114
|
+
return self.generation_model
|
|
115
|
+
elif purpose == Purpose.CORRECTION:
|
|
116
|
+
return self.correction_model
|
|
117
|
+
elif purpose == Purpose.SUPERVISION:
|
|
118
|
+
return self.supervision_model
|
|
119
|
+
else:
|
|
120
|
+
return self.execution_model # Default fallback
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class StreamChunk:
|
|
125
|
+
"""A normalized chunk from a streaming LLM response.
|
|
126
|
+
|
|
127
|
+
Provider-specific streaming formats are translated into this common type
|
|
128
|
+
by each :class:`LLMProvider` implementation.
|
|
129
|
+
|
|
130
|
+
Attributes:
|
|
131
|
+
type: Event type — one of ``"text_delta"``, ``"thinking_delta"``,
|
|
132
|
+
``"tool_use_start"``, ``"tool_use_stop"``, ``"message_stop"``.
|
|
133
|
+
text: Text content for ``text_delta`` and ``thinking_delta`` types.
|
|
134
|
+
tool_id: Tool call ID for ``tool_use_start``.
|
|
135
|
+
tool_name: Tool name for ``tool_use_start``.
|
|
136
|
+
tool_input: Tool input dict for ``tool_use_start`` (may be empty;
|
|
137
|
+
final inputs are provided in the ``message_stop`` chunk).
|
|
138
|
+
input_tokens: Input token count, populated for ``message_stop``.
|
|
139
|
+
output_tokens: Output token count, populated for ``message_stop``.
|
|
140
|
+
stop_reason: Why the model stopped, populated for ``message_stop``.
|
|
141
|
+
tool_inputs_by_id: Mapping of tool_id → final input dict, populated
|
|
142
|
+
for ``message_stop``. More reliable than streaming incremental
|
|
143
|
+
input deltas.
|
|
144
|
+
|
|
145
|
+
.. note:: ``tool_use_stop`` ordering differs by provider:
|
|
146
|
+
|
|
147
|
+
- **Anthropic**: emitted immediately when each tool call's content
|
|
148
|
+
block ends (``content_block_stop`` event), so consumers see
|
|
149
|
+
``tool_use_start → [deltas] → tool_use_stop`` interleaved.
|
|
150
|
+
- **OpenAI-compatible**: emitted after the full stream ends (before
|
|
151
|
+
``message_stop``), because the SSE protocol has no per-tool stop
|
|
152
|
+
marker. All ``tool_use_stop`` chunks arrive together at the end.
|
|
153
|
+
|
|
154
|
+
Consumers MUST use ``tool_inputs_by_id`` from the ``message_stop``
|
|
155
|
+
chunk for final tool inputs rather than relying on ``tool_use_stop``
|
|
156
|
+
ordering.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
type: str
|
|
160
|
+
text: Optional[str] = None
|
|
161
|
+
tool_id: Optional[str] = None
|
|
162
|
+
tool_name: Optional[str] = None
|
|
163
|
+
tool_input: Optional[dict] = None
|
|
164
|
+
input_tokens: Optional[int] = None
|
|
165
|
+
output_tokens: Optional[int] = None
|
|
166
|
+
stop_reason: Optional[str] = None
|
|
167
|
+
tool_inputs_by_id: Optional[dict] = None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class ToolCall:
|
|
172
|
+
"""Represents a tool call requested by the LLM.
|
|
173
|
+
|
|
174
|
+
Attributes:
|
|
175
|
+
id: Unique identifier for this tool call
|
|
176
|
+
name: Name of the tool to call
|
|
177
|
+
input: Input arguments for the tool (as dict)
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
id: str
|
|
181
|
+
name: str
|
|
182
|
+
input: dict
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class ToolResult:
|
|
187
|
+
"""Result of executing a tool call.
|
|
188
|
+
|
|
189
|
+
Attributes:
|
|
190
|
+
tool_call_id: ID of the tool call this is responding to
|
|
191
|
+
content: Result content (string or structured data)
|
|
192
|
+
is_error: Whether this result represents an error
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
tool_call_id: str
|
|
196
|
+
content: str
|
|
197
|
+
is_error: bool = False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class LLMResponse:
|
|
202
|
+
"""Response from an LLM completion.
|
|
203
|
+
|
|
204
|
+
Attributes:
|
|
205
|
+
content: Text content of the response (may be empty if tool calls)
|
|
206
|
+
tool_calls: List of tool calls requested by the model
|
|
207
|
+
stop_reason: Why the model stopped generating
|
|
208
|
+
model: Model that generated this response
|
|
209
|
+
input_tokens: Number of input tokens used
|
|
210
|
+
output_tokens: Number of output tokens generated
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
content: str
|
|
214
|
+
tool_calls: list[ToolCall] = field(default_factory=list)
|
|
215
|
+
stop_reason: str = "end_turn"
|
|
216
|
+
model: str = ""
|
|
217
|
+
input_tokens: int = 0
|
|
218
|
+
output_tokens: int = 0
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def has_tool_calls(self) -> bool:
|
|
222
|
+
"""Check if response contains tool calls."""
|
|
223
|
+
return len(self.tool_calls) > 0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class Message:
|
|
228
|
+
"""A message in a conversation.
|
|
229
|
+
|
|
230
|
+
Attributes:
|
|
231
|
+
role: Message role ("user", "assistant", "system")
|
|
232
|
+
content: Message content
|
|
233
|
+
tool_calls: Tool calls (for assistant messages)
|
|
234
|
+
tool_results: Tool results (for user messages responding to tool calls)
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
role: str
|
|
238
|
+
content: str
|
|
239
|
+
tool_calls: list[ToolCall] = field(default_factory=list)
|
|
240
|
+
tool_results: list[ToolResult] = field(default_factory=list)
|
|
241
|
+
|
|
242
|
+
def to_dict(self) -> dict:
|
|
243
|
+
"""Convert to dict format for API calls."""
|
|
244
|
+
result = {"role": self.role, "content": self.content}
|
|
245
|
+
if self.tool_calls:
|
|
246
|
+
result["tool_calls"] = [
|
|
247
|
+
{"id": tc.id, "name": tc.name, "input": tc.input}
|
|
248
|
+
for tc in self.tool_calls
|
|
249
|
+
]
|
|
250
|
+
if self.tool_results:
|
|
251
|
+
result["tool_results"] = [
|
|
252
|
+
{
|
|
253
|
+
"tool_call_id": tr.tool_call_id,
|
|
254
|
+
"content": tr.content,
|
|
255
|
+
"is_error": tr.is_error,
|
|
256
|
+
}
|
|
257
|
+
for tr in self.tool_results
|
|
258
|
+
]
|
|
259
|
+
return result
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass
|
|
263
|
+
class Tool:
|
|
264
|
+
"""Definition of a tool the LLM can use.
|
|
265
|
+
|
|
266
|
+
Attributes:
|
|
267
|
+
name: Tool name
|
|
268
|
+
description: What the tool does
|
|
269
|
+
input_schema: JSON schema for tool input
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
name: str
|
|
273
|
+
description: str
|
|
274
|
+
input_schema: dict
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class LLMProvider(ABC):
|
|
278
|
+
"""Abstract base class for LLM providers.
|
|
279
|
+
|
|
280
|
+
Implementations must provide complete() and optionally stream().
|
|
281
|
+
Model selection is handled via the purpose parameter.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
def __init__(self, model_selector: Optional[ModelSelector] = None):
|
|
285
|
+
"""Initialize the provider.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
model_selector: Custom model selector (uses defaults if None)
|
|
289
|
+
"""
|
|
290
|
+
self.model_selector = model_selector or ModelSelector()
|
|
291
|
+
|
|
292
|
+
@abstractmethod
|
|
293
|
+
def complete(
|
|
294
|
+
self,
|
|
295
|
+
messages: list[dict],
|
|
296
|
+
purpose: Purpose = Purpose.EXECUTION,
|
|
297
|
+
tools: Optional[list[Tool]] = None,
|
|
298
|
+
max_tokens: int = 4096,
|
|
299
|
+
temperature: float = 0.0,
|
|
300
|
+
system: Optional[str] = None,
|
|
301
|
+
) -> LLMResponse:
|
|
302
|
+
"""Generate a completion.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
messages: Conversation messages
|
|
306
|
+
purpose: Purpose of call (for model selection)
|
|
307
|
+
tools: Available tools for the model to use
|
|
308
|
+
max_tokens: Maximum tokens to generate
|
|
309
|
+
temperature: Sampling temperature
|
|
310
|
+
system: System prompt
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
LLMResponse with content and/or tool calls
|
|
314
|
+
"""
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
def stream(
|
|
318
|
+
self,
|
|
319
|
+
messages: list[dict],
|
|
320
|
+
purpose: Purpose = Purpose.EXECUTION,
|
|
321
|
+
max_tokens: int = 4096,
|
|
322
|
+
temperature: float = 0.0,
|
|
323
|
+
system: Optional[str] = None,
|
|
324
|
+
) -> Iterator[str]:
|
|
325
|
+
"""Stream a completion token by token.
|
|
326
|
+
|
|
327
|
+
Default implementation falls back to complete() and yields full response.
|
|
328
|
+
Override for true streaming support.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
messages: Conversation messages
|
|
332
|
+
purpose: Purpose of call (for model selection)
|
|
333
|
+
max_tokens: Maximum tokens to generate
|
|
334
|
+
temperature: Sampling temperature
|
|
335
|
+
system: System prompt
|
|
336
|
+
|
|
337
|
+
Yields:
|
|
338
|
+
Text chunks as they are generated
|
|
339
|
+
"""
|
|
340
|
+
response = self.complete(
|
|
341
|
+
messages=messages,
|
|
342
|
+
purpose=purpose,
|
|
343
|
+
max_tokens=max_tokens,
|
|
344
|
+
temperature=temperature,
|
|
345
|
+
system=system,
|
|
346
|
+
)
|
|
347
|
+
yield response.content
|
|
348
|
+
|
|
349
|
+
async def async_complete(
|
|
350
|
+
self,
|
|
351
|
+
messages: list[dict],
|
|
352
|
+
purpose: Purpose = Purpose.EXECUTION,
|
|
353
|
+
tools: Optional[list["Tool"]] = None,
|
|
354
|
+
max_tokens: int = 4096,
|
|
355
|
+
temperature: float = 0.0,
|
|
356
|
+
system: Optional[str] = None,
|
|
357
|
+
) -> "LLMResponse":
|
|
358
|
+
"""Async completion.
|
|
359
|
+
|
|
360
|
+
Default implementation wraps the synchronous :meth:`complete` in a
|
|
361
|
+
thread-pool executor so it never blocks the event loop. Subclasses
|
|
362
|
+
should override this with a truly async implementation when the
|
|
363
|
+
underlying SDK supports it.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
messages: Conversation messages
|
|
367
|
+
purpose: Purpose of call (for model selection)
|
|
368
|
+
tools: Available tools for the model to use
|
|
369
|
+
max_tokens: Maximum tokens to generate
|
|
370
|
+
temperature: Sampling temperature
|
|
371
|
+
system: System prompt
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
LLMResponse with content and/or tool calls
|
|
375
|
+
"""
|
|
376
|
+
loop = asyncio.get_running_loop()
|
|
377
|
+
return await loop.run_in_executor(
|
|
378
|
+
None,
|
|
379
|
+
lambda: self.complete(messages, purpose, tools, max_tokens, temperature, system),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def supports(self, capability: str) -> bool:
|
|
383
|
+
"""Check whether this provider supports an optional capability.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
capability: Capability name, e.g. ``"extended_thinking"``.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
``True`` if the capability is supported, ``False`` otherwise.
|
|
390
|
+
"""
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
# Not decorated with @abstractmethod intentionally: providers that only
|
|
394
|
+
# support synchronous completion (e.g. thin wrappers) don't need to
|
|
395
|
+
# implement streaming. Calling async_stream() on such a provider raises
|
|
396
|
+
# NotImplementedError at call time rather than at instantiation.
|
|
397
|
+
async def async_stream(
|
|
398
|
+
self,
|
|
399
|
+
messages: list[dict],
|
|
400
|
+
system: str,
|
|
401
|
+
tools: list[dict],
|
|
402
|
+
model: str,
|
|
403
|
+
max_tokens: int,
|
|
404
|
+
interrupt_event: Optional[asyncio.Event] = None,
|
|
405
|
+
extended_thinking: bool = False,
|
|
406
|
+
) -> AsyncIterator["StreamChunk"]:
|
|
407
|
+
"""Stream a completion as normalized :class:`StreamChunk` objects.
|
|
408
|
+
|
|
409
|
+
Subclasses should override this with a provider-specific implementation.
|
|
410
|
+
The default raises :exc:`NotImplementedError`.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
messages: Conversation messages in the provider's expected format.
|
|
414
|
+
system: System prompt string.
|
|
415
|
+
tools: Already-serialized tool definitions (list of dicts).
|
|
416
|
+
model: Model identifier to use for this call.
|
|
417
|
+
max_tokens: Maximum output tokens.
|
|
418
|
+
interrupt_event: When set, the stream should stop at the next
|
|
419
|
+
opportunity.
|
|
420
|
+
extended_thinking: When ``True``, request extended thinking tokens
|
|
421
|
+
from providers that support them (see :meth:`supports`).
|
|
422
|
+
Providers that do not support this capability should silently
|
|
423
|
+
ignore the flag.
|
|
424
|
+
|
|
425
|
+
Yields:
|
|
426
|
+
:class:`StreamChunk` objects in order of generation.
|
|
427
|
+
"""
|
|
428
|
+
raise NotImplementedError(
|
|
429
|
+
f"{type(self).__name__} does not implement async_stream(). "
|
|
430
|
+
"Override this method in your provider subclass."
|
|
431
|
+
)
|
|
432
|
+
if False: # pragma: no cover # makes this an async generator
|
|
433
|
+
yield # type: ignore[misc]
|
|
434
|
+
|
|
435
|
+
def get_model(self, purpose: Purpose) -> str:
|
|
436
|
+
"""Get the model for a given purpose.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
purpose: The purpose of the LLM call
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Model identifier string
|
|
443
|
+
"""
|
|
444
|
+
return self.model_selector.for_purpose(purpose)
|