devsquad 3.6.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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
LLM Backend Abstraction Layer
|
|
5
|
+
|
|
6
|
+
Provides a pluggable interface for Worker to execute prompts against
|
|
7
|
+
different LLM backends. Default is MockBackend (returns assembled prompt).
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
# Default (mock) - returns assembled prompt as-is
|
|
11
|
+
worker = Worker(..., llm_backend=None)
|
|
12
|
+
|
|
13
|
+
# Custom backend (API keys from environment variables)
|
|
14
|
+
from scripts.collaboration.llm_backend import OpenAIBackend
|
|
15
|
+
import os
|
|
16
|
+
backend = OpenAIBackend(api_key=os.environ["OPENAI_API_KEY"], model="gpt-4")
|
|
17
|
+
worker = Worker(..., llm_backend=backend)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from typing import Dict, Any, Optional, Generator
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LLMBackend(ABC):
|
|
25
|
+
"""Abstract base class for LLM execution backends."""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Generate a response from the LLM given a prompt.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
prompt: The assembled prompt/instruction text.
|
|
34
|
+
**kwargs: Backend-specific parameters (temperature, max_tokens, etc.)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: The LLM's response text.
|
|
38
|
+
"""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def is_available(self) -> bool:
|
|
43
|
+
"""Check if the backend is properly configured and available."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def generate_stream(self, prompt: str, **kwargs) -> Generator[str, None, None]:
|
|
47
|
+
"""
|
|
48
|
+
Stream a response from the LLM, yielding chunks as they arrive.
|
|
49
|
+
|
|
50
|
+
Default implementation falls back to generate() and yields the full response.
|
|
51
|
+
Subclasses should override for true streaming support.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
prompt: The assembled prompt/instruction text.
|
|
55
|
+
**kwargs: Backend-specific parameters.
|
|
56
|
+
|
|
57
|
+
Yields:
|
|
58
|
+
str: Chunks of the LLM's response text.
|
|
59
|
+
"""
|
|
60
|
+
yield self.generate(prompt, **kwargs)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MockBackend(LLMBackend):
|
|
64
|
+
"""
|
|
65
|
+
Default backend that generates a formatted mock analysis.
|
|
66
|
+
|
|
67
|
+
Instead of returning raw prompt text, MockBackend produces a readable
|
|
68
|
+
mock analysis with [MOCK MODE] markers so users can distinguish it
|
|
69
|
+
from real LLM output.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
73
|
+
role_name = kwargs.get("role_name", "AI Assistant")
|
|
74
|
+
task_desc = kwargs.get("task_description", "")
|
|
75
|
+
lines = [
|
|
76
|
+
f"[MOCK MODE] {role_name} Analysis",
|
|
77
|
+
"=" * 50,
|
|
78
|
+
"",
|
|
79
|
+
f"Task: {task_desc}" if task_desc else "Task: (auto-detected)",
|
|
80
|
+
"",
|
|
81
|
+
"This is a mock response. To get real AI analysis,",
|
|
82
|
+
"set --backend openai (or anthropic) with a valid API key.",
|
|
83
|
+
"",
|
|
84
|
+
f"Prompt length: {len(prompt)} chars",
|
|
85
|
+
]
|
|
86
|
+
return "\n".join(lines)
|
|
87
|
+
|
|
88
|
+
def is_available(self) -> bool:
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TraeBackend(LLMBackend):
|
|
93
|
+
"""
|
|
94
|
+
Backend for Trae IDE's built-in AI.
|
|
95
|
+
|
|
96
|
+
In Trae IDE, the AI host executes the prompt. This backend is a
|
|
97
|
+
passthrough that signals the host to execute.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
101
|
+
return prompt
|
|
102
|
+
|
|
103
|
+
def is_available(self) -> bool:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class OpenAIBackend(LLMBackend):
|
|
108
|
+
DEFAULT_TIMEOUT = 120
|
|
109
|
+
MAX_RETRIES = 3
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
api_key: Optional[str] = None,
|
|
114
|
+
model: str = "gpt-4",
|
|
115
|
+
base_url: Optional[str] = None,
|
|
116
|
+
temperature: float = 0.7,
|
|
117
|
+
max_tokens: int = 4096,
|
|
118
|
+
timeout: Optional[int] = None,
|
|
119
|
+
):
|
|
120
|
+
self._api_key = api_key
|
|
121
|
+
self.model = model
|
|
122
|
+
self.base_url = base_url
|
|
123
|
+
self.temperature = temperature
|
|
124
|
+
self.max_tokens = max_tokens
|
|
125
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
|
126
|
+
self._client = None
|
|
127
|
+
self._client_lock = __import__('threading').Lock()
|
|
128
|
+
|
|
129
|
+
def __repr__(self):
|
|
130
|
+
return f"OpenAIBackend(model={self.model}, base_url={self.base_url})"
|
|
131
|
+
|
|
132
|
+
def _get_client(self):
|
|
133
|
+
if self._client is None:
|
|
134
|
+
with self._client_lock:
|
|
135
|
+
if self._client is None:
|
|
136
|
+
try:
|
|
137
|
+
from openai import OpenAI
|
|
138
|
+
kwargs = {"api_key": self._api_key, "timeout": self.timeout}
|
|
139
|
+
if self.base_url:
|
|
140
|
+
kwargs["base_url"] = self.base_url
|
|
141
|
+
self._client = OpenAI(**kwargs)
|
|
142
|
+
except ImportError:
|
|
143
|
+
raise ImportError("openai package required: pip install openai")
|
|
144
|
+
return self._client
|
|
145
|
+
|
|
146
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
147
|
+
import time
|
|
148
|
+
client = self._get_client()
|
|
149
|
+
last_error = None
|
|
150
|
+
for attempt in range(self.MAX_RETRIES):
|
|
151
|
+
try:
|
|
152
|
+
response = client.chat.completions.create(
|
|
153
|
+
model=kwargs.get("model", self.model),
|
|
154
|
+
messages=[{"role": "user", "content": prompt}],
|
|
155
|
+
temperature=kwargs.get("temperature", self.temperature),
|
|
156
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
157
|
+
)
|
|
158
|
+
return response.choices[0].message.content or ""
|
|
159
|
+
except Exception as e:
|
|
160
|
+
last_error = e
|
|
161
|
+
if attempt < self.MAX_RETRIES - 1:
|
|
162
|
+
time.sleep(2 ** attempt)
|
|
163
|
+
raise last_error or RuntimeError(f"OpenAI generate failed after {self.MAX_RETRIES} attempts")
|
|
164
|
+
|
|
165
|
+
def generate_stream(self, prompt: str, **kwargs) -> Generator[str, None, None]:
|
|
166
|
+
client = self._get_client()
|
|
167
|
+
stream = client.chat.completions.create(
|
|
168
|
+
model=kwargs.get("model", self.model),
|
|
169
|
+
messages=[{"role": "user", "content": prompt}],
|
|
170
|
+
temperature=kwargs.get("temperature", self.temperature),
|
|
171
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
172
|
+
stream=True,
|
|
173
|
+
)
|
|
174
|
+
for chunk in stream:
|
|
175
|
+
content = chunk.choices[0].delta.content
|
|
176
|
+
if content:
|
|
177
|
+
yield content
|
|
178
|
+
|
|
179
|
+
def is_available(self) -> bool:
|
|
180
|
+
try:
|
|
181
|
+
self._get_client()
|
|
182
|
+
return True
|
|
183
|
+
except Exception:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class AnthropicBackend(LLMBackend):
|
|
188
|
+
DEFAULT_TIMEOUT = 120
|
|
189
|
+
MAX_RETRIES = 3
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
api_key: Optional[str] = None,
|
|
194
|
+
model: str = "claude-sonnet-4-20250514",
|
|
195
|
+
base_url: Optional[str] = None,
|
|
196
|
+
max_tokens: int = 4096,
|
|
197
|
+
timeout: Optional[int] = None,
|
|
198
|
+
):
|
|
199
|
+
self._api_key = api_key
|
|
200
|
+
self.model = model
|
|
201
|
+
self.base_url = base_url
|
|
202
|
+
self.max_tokens = max_tokens
|
|
203
|
+
self.timeout = timeout or self.DEFAULT_TIMEOUT
|
|
204
|
+
self._client = None
|
|
205
|
+
self._client_lock = __import__('threading').Lock()
|
|
206
|
+
|
|
207
|
+
def __repr__(self):
|
|
208
|
+
return f"AnthropicBackend(model={self.model}, base_url={self.base_url})"
|
|
209
|
+
|
|
210
|
+
def _get_client(self):
|
|
211
|
+
if self._client is None:
|
|
212
|
+
with self._client_lock:
|
|
213
|
+
if self._client is None:
|
|
214
|
+
try:
|
|
215
|
+
from anthropic import Anthropic
|
|
216
|
+
kwargs = {"api_key": self._api_key, "timeout": self.timeout}
|
|
217
|
+
if self.base_url:
|
|
218
|
+
kwargs["base_url"] = self.base_url
|
|
219
|
+
self._client = Anthropic(**kwargs)
|
|
220
|
+
except ImportError:
|
|
221
|
+
raise ImportError("anthropic package required: pip install anthropic")
|
|
222
|
+
return self._client
|
|
223
|
+
|
|
224
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
225
|
+
import time
|
|
226
|
+
client = self._get_client()
|
|
227
|
+
last_error = None
|
|
228
|
+
for attempt in range(self.MAX_RETRIES):
|
|
229
|
+
try:
|
|
230
|
+
response = client.messages.create(
|
|
231
|
+
model=kwargs.get("model", self.model),
|
|
232
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
233
|
+
messages=[{"role": "user", "content": prompt}],
|
|
234
|
+
)
|
|
235
|
+
return response.content[0].text if response.content else ""
|
|
236
|
+
except Exception as e:
|
|
237
|
+
last_error = e
|
|
238
|
+
if attempt < self.MAX_RETRIES - 1:
|
|
239
|
+
time.sleep(2 ** attempt)
|
|
240
|
+
raise last_error or RuntimeError(f"Anthropic generate failed after {self.MAX_RETRIES} attempts")
|
|
241
|
+
|
|
242
|
+
def generate_stream(self, prompt: str, **kwargs) -> Generator[str, None, None]:
|
|
243
|
+
client = self._get_client()
|
|
244
|
+
with client.messages.stream(
|
|
245
|
+
model=kwargs.get("model", self.model),
|
|
246
|
+
max_tokens=kwargs.get("max_tokens", self.max_tokens),
|
|
247
|
+
messages=[{"role": "user", "content": prompt}],
|
|
248
|
+
) as stream:
|
|
249
|
+
for text in stream.text_stream:
|
|
250
|
+
yield text
|
|
251
|
+
|
|
252
|
+
def is_available(self) -> bool:
|
|
253
|
+
try:
|
|
254
|
+
self._get_client()
|
|
255
|
+
return True
|
|
256
|
+
except Exception:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class FallbackBackend(LLMBackend):
|
|
261
|
+
"""
|
|
262
|
+
Backend with automatic failover across multiple backends.
|
|
263
|
+
|
|
264
|
+
Tries each backend in order. If the primary fails (network error,
|
|
265
|
+
rate limit, auth error, etc.), automatically falls back to the next.
|
|
266
|
+
|
|
267
|
+
Usage:
|
|
268
|
+
primary = AnthropicBackend(api_key="...", model="claude-sonnet-4-6")
|
|
269
|
+
fallback = OpenAIBackend(api_key="...", model="gpt-5.5")
|
|
270
|
+
backend = FallbackBackend([primary, fallback])
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
def __init__(self, backends: list, cooldown_seconds: float = 30.0):
|
|
274
|
+
if not backends:
|
|
275
|
+
raise ValueError("FallbackBackend requires at least one backend")
|
|
276
|
+
self._backends = backends
|
|
277
|
+
self._cooldown_seconds = cooldown_seconds
|
|
278
|
+
self._failed_at: Dict[str, float] = {}
|
|
279
|
+
self._active_index = 0
|
|
280
|
+
self._lock = __import__('threading').Lock()
|
|
281
|
+
|
|
282
|
+
def __repr__(self):
|
|
283
|
+
names = [type(b).__name__ for b in self._backends]
|
|
284
|
+
return f"FallbackBackend({names})"
|
|
285
|
+
|
|
286
|
+
def _is_cooled_down(self, backend_repr: str) -> bool:
|
|
287
|
+
import time
|
|
288
|
+
failed_time = self._failed_at.get(backend_repr, 0)
|
|
289
|
+
return (time.time() - failed_time) > self._cooldown_seconds
|
|
290
|
+
|
|
291
|
+
def _mark_failed(self, backend_repr: str):
|
|
292
|
+
import time
|
|
293
|
+
self._failed_at[backend_repr] = time.time()
|
|
294
|
+
|
|
295
|
+
def generate(self, prompt: str, **kwargs) -> str:
|
|
296
|
+
import logging
|
|
297
|
+
logger = logging.getLogger(__name__)
|
|
298
|
+
last_error = None
|
|
299
|
+
|
|
300
|
+
with self._lock:
|
|
301
|
+
ordered = list(range(len(self._backends)))
|
|
302
|
+
ordered.sort(key=lambda i: (i != self._active_index, i))
|
|
303
|
+
|
|
304
|
+
for idx in ordered:
|
|
305
|
+
backend = self._backends[idx]
|
|
306
|
+
backend_repr = repr(backend)
|
|
307
|
+
|
|
308
|
+
if idx != self._active_index and not self._is_cooled_down(backend_repr):
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
result = backend.generate(prompt, **kwargs)
|
|
313
|
+
with self._lock:
|
|
314
|
+
self._active_index = idx
|
|
315
|
+
if idx != 0:
|
|
316
|
+
logger.info("FallbackBackend: switched to %s", backend_repr)
|
|
317
|
+
return result
|
|
318
|
+
except Exception as e:
|
|
319
|
+
last_error = e
|
|
320
|
+
self._mark_failed(backend_repr)
|
|
321
|
+
logger.warning(
|
|
322
|
+
"FallbackBackend: %s failed (%s), trying next",
|
|
323
|
+
backend_repr, type(e).__name__,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
raise last_error or RuntimeError("All backends failed with no specific error")
|
|
327
|
+
|
|
328
|
+
def generate_stream(self, prompt: str, **kwargs) -> Generator[str, None, None]:
|
|
329
|
+
import logging
|
|
330
|
+
logger = logging.getLogger(__name__)
|
|
331
|
+
last_error = None
|
|
332
|
+
|
|
333
|
+
with self._lock:
|
|
334
|
+
ordered = list(range(len(self._backends)))
|
|
335
|
+
ordered.sort(key=lambda i: (i != self._active_index, i))
|
|
336
|
+
|
|
337
|
+
for idx in ordered:
|
|
338
|
+
backend = self._backends[idx]
|
|
339
|
+
backend_repr = repr(backend)
|
|
340
|
+
|
|
341
|
+
if idx != self._active_index and not self._is_cooled_down(backend_repr):
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
with self._lock:
|
|
346
|
+
self._active_index = idx
|
|
347
|
+
for chunk in backend.generate_stream(prompt, **kwargs):
|
|
348
|
+
yield chunk
|
|
349
|
+
return
|
|
350
|
+
except Exception as e:
|
|
351
|
+
last_error = e
|
|
352
|
+
self._mark_failed(backend_repr)
|
|
353
|
+
logger.warning(
|
|
354
|
+
"FallbackBackend: %s stream failed (%s), trying next",
|
|
355
|
+
backend_repr, type(e).__name__,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
raise last_error or RuntimeError("All backends failed with no specific error")
|
|
359
|
+
|
|
360
|
+
def is_available(self) -> bool:
|
|
361
|
+
return any(b.is_available() for b in self._backends)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def create_backend(backend_type: str = "mock", **kwargs) -> LLMBackend:
|
|
365
|
+
"""
|
|
366
|
+
Factory function to create an LLM backend by type name.
|
|
367
|
+
|
|
368
|
+
Automatically reads configuration from environment variables when not
|
|
369
|
+
explicitly provided via kwargs. Supports .env file loading.
|
|
370
|
+
|
|
371
|
+
Environment Variables:
|
|
372
|
+
DEVSQUAD_LLM_BACKEND: Default backend type (mock|trae|openai|anthropic)
|
|
373
|
+
DEVSQUAD_OPENAI_API_KEY: OpenAI API key
|
|
374
|
+
DEVSQUAD_OPENAI_BASE_URL: OpenAI-compatible base URL
|
|
375
|
+
DEVSQUAD_OPENAI_MODEL: OpenAI model name
|
|
376
|
+
DEVSQUAD_ANTHROPIC_API_KEY: Anthropic API key
|
|
377
|
+
DEVSQUAD_ANTHROPIC_BASE_URL: Anthropic-compatible base URL
|
|
378
|
+
DEVSQUAD_ANTHROPIC_MODEL: Anthropic model name
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
backend_type: One of 'mock', 'trae', 'openai', 'anthropic'.
|
|
382
|
+
If not specified, reads from DEVSQUAD_LLM_BACKEND env var.
|
|
383
|
+
**kwargs: Backend-specific configuration (overrides env vars)
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
LLMBackend instance
|
|
387
|
+
"""
|
|
388
|
+
import os
|
|
389
|
+
|
|
390
|
+
_load_dotenv()
|
|
391
|
+
|
|
392
|
+
env_backend = os.environ.get("DEVSQUAD_LLM_BACKEND", "mock").lower()
|
|
393
|
+
|
|
394
|
+
if backend_type == "mock" and not kwargs:
|
|
395
|
+
if env_backend in ("openai", "anthropic", "fallback"):
|
|
396
|
+
backend_type = env_backend
|
|
397
|
+
|
|
398
|
+
if backend_type == "fallback":
|
|
399
|
+
anthropic_key = kwargs.pop("anthropic_api_key", None) or os.environ.get("DEVSQUAD_ANTHROPIC_API_KEY")
|
|
400
|
+
openai_key = kwargs.pop("openai_api_key", None) or os.environ.get("DEVSQUAD_OPENAI_API_KEY")
|
|
401
|
+
backends_list = []
|
|
402
|
+
if anthropic_key:
|
|
403
|
+
backends_list.append(AnthropicBackend(
|
|
404
|
+
api_key=anthropic_key,
|
|
405
|
+
base_url=kwargs.pop("anthropic_base_url", None) or os.environ.get("DEVSQUAD_ANTHROPIC_BASE_URL"),
|
|
406
|
+
model=kwargs.pop("anthropic_model", None) or os.environ.get("DEVSQUAD_ANTHROPIC_MODEL", "claude-sonnet-4-20250514"),
|
|
407
|
+
max_tokens=kwargs.pop("max_tokens", 4096),
|
|
408
|
+
timeout=kwargs.pop("timeout", None),
|
|
409
|
+
))
|
|
410
|
+
if openai_key:
|
|
411
|
+
backends_list.append(OpenAIBackend(
|
|
412
|
+
api_key=openai_key,
|
|
413
|
+
base_url=kwargs.pop("openai_base_url", None) or os.environ.get("DEVSQUAD_OPENAI_BASE_URL"),
|
|
414
|
+
model=kwargs.pop("openai_model", None) or os.environ.get("DEVSQUAD_OPENAI_MODEL", "gpt-4"),
|
|
415
|
+
max_tokens=kwargs.pop("max_tokens", 4096),
|
|
416
|
+
timeout=kwargs.pop("timeout", None),
|
|
417
|
+
))
|
|
418
|
+
if not backends_list:
|
|
419
|
+
backends_list.append(MockBackend())
|
|
420
|
+
return FallbackBackend(backends_list, cooldown_seconds=kwargs.pop("cooldown_seconds", 30.0))
|
|
421
|
+
|
|
422
|
+
backends = {
|
|
423
|
+
"mock": MockBackend,
|
|
424
|
+
"trae": TraeBackend,
|
|
425
|
+
"openai": OpenAIBackend,
|
|
426
|
+
"anthropic": AnthropicBackend,
|
|
427
|
+
}
|
|
428
|
+
cls = backends.get(backend_type.lower())
|
|
429
|
+
if cls is None:
|
|
430
|
+
raise ValueError(f"Unknown backend type: {backend_type}. Available: {list(backends.keys())}")
|
|
431
|
+
|
|
432
|
+
if cls == OpenAIBackend:
|
|
433
|
+
kwargs.setdefault("api_key", os.environ.get("DEVSQUAD_OPENAI_API_KEY"))
|
|
434
|
+
kwargs.setdefault("base_url", os.environ.get("DEVSQUAD_OPENAI_BASE_URL"))
|
|
435
|
+
kwargs.setdefault("model", os.environ.get("DEVSQUAD_OPENAI_MODEL", "gpt-4"))
|
|
436
|
+
elif cls == AnthropicBackend:
|
|
437
|
+
kwargs.setdefault("api_key", os.environ.get("DEVSQUAD_ANTHROPIC_API_KEY"))
|
|
438
|
+
kwargs.setdefault("base_url", os.environ.get("DEVSQUAD_ANTHROPIC_BASE_URL"))
|
|
439
|
+
kwargs.setdefault("model", os.environ.get("DEVSQUAD_ANTHROPIC_MODEL", "claude-sonnet-4-20250514"))
|
|
440
|
+
|
|
441
|
+
return cls(**kwargs)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _load_dotenv():
|
|
445
|
+
"""Load .env file if python-dotenv is available."""
|
|
446
|
+
try:
|
|
447
|
+
from dotenv import load_dotenv
|
|
448
|
+
from pathlib import Path
|
|
449
|
+
env_path = Path(__file__).parent.parent.parent / ".env"
|
|
450
|
+
if env_path.exists():
|
|
451
|
+
load_dotenv(env_path)
|
|
452
|
+
except ImportError:
|
|
453
|
+
pass
|