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.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. 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