codeoptix 0.1.3__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 (92) hide show
  1. codeoptix/__init__.py +8 -0
  2. codeoptix/acp/__init__.py +33 -0
  3. codeoptix/acp/agent.py +209 -0
  4. codeoptix/acp/bridge.py +402 -0
  5. codeoptix/acp/client_adapter.py +312 -0
  6. codeoptix/acp/code_extractor.py +125 -0
  7. codeoptix/acp/orchestrator.py +349 -0
  8. codeoptix/acp/registry.py +294 -0
  9. codeoptix/adapters/__init__.py +18 -0
  10. codeoptix/adapters/base.py +50 -0
  11. codeoptix/adapters/basic.py +195 -0
  12. codeoptix/adapters/claude_code.py +221 -0
  13. codeoptix/adapters/codex.py +327 -0
  14. codeoptix/adapters/factory.py +56 -0
  15. codeoptix/adapters/gemini_cli.py +370 -0
  16. codeoptix/artifacts/__init__.py +5 -0
  17. codeoptix/artifacts/manager.py +193 -0
  18. codeoptix/behaviors/__init__.py +45 -0
  19. codeoptix/behaviors/base.py +81 -0
  20. codeoptix/behaviors/insecure_code.py +129 -0
  21. codeoptix/behaviors/plan_drift.py +192 -0
  22. codeoptix/behaviors/vacuous_tests.py +198 -0
  23. codeoptix/cli.py +1468 -0
  24. codeoptix/evaluation/__init__.py +23 -0
  25. codeoptix/evaluation/bloom_integration.py +271 -0
  26. codeoptix/evaluation/engine.py +274 -0
  27. codeoptix/evaluation/evaluators.py +308 -0
  28. codeoptix/evaluation/scenario_generator.py +222 -0
  29. codeoptix/evolution/__init__.py +7 -0
  30. codeoptix/evolution/engine.py +206 -0
  31. codeoptix/evolution/gepa_integration.py +149 -0
  32. codeoptix/evolution/proposer.py +185 -0
  33. codeoptix/linters/__init__.py +13 -0
  34. codeoptix/linters/bandit_linter.py +172 -0
  35. codeoptix/linters/base.py +105 -0
  36. codeoptix/linters/coverage_linter.py +156 -0
  37. codeoptix/linters/flake8_linter.py +156 -0
  38. codeoptix/linters/html_accessibility_linter.py +374 -0
  39. codeoptix/linters/language_detector.py +150 -0
  40. codeoptix/linters/mypy_linter.py +184 -0
  41. codeoptix/linters/pip_audit_linter.py +152 -0
  42. codeoptix/linters/pylint_linter.py +198 -0
  43. codeoptix/linters/ruff_linter.py +206 -0
  44. codeoptix/linters/runner.py +186 -0
  45. codeoptix/linters/safety_linter.py +184 -0
  46. codeoptix/reflection/__init__.py +6 -0
  47. codeoptix/reflection/engine.py +70 -0
  48. codeoptix/reflection/generator.py +209 -0
  49. codeoptix/utils/__init__.py +1 -0
  50. codeoptix/utils/config.py +91 -0
  51. codeoptix/utils/llm.py +334 -0
  52. codeoptix/utils/retry.py +133 -0
  53. codeoptix/vendor/__init__.py +2 -0
  54. codeoptix/vendor/bloom/README.md +26 -0
  55. codeoptix/vendor/bloom/__init__.py +11 -0
  56. codeoptix/vendor/bloom/globals.py +39 -0
  57. codeoptix/vendor/bloom/orchestrators/ConversationOrchestrator.py +450 -0
  58. codeoptix/vendor/bloom/orchestrators/SimEnvOrchestrator.py +839 -0
  59. codeoptix/vendor/bloom/prompts/configurable_prompts/README.md +85 -0
  60. codeoptix/vendor/bloom/prompts/configurable_prompts/default.json +18 -0
  61. codeoptix/vendor/bloom/prompts/configurable_prompts/ideation-default.json +18 -0
  62. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_animal-welfare.json +18 -0
  63. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_contextual-optimism.json +18 -0
  64. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defend-objects.json +18 -0
  65. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defer-to-users.json +18 -0
  66. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_emotional-bond.json +18 -0
  67. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_flattery.json +18 -0
  68. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_hardcode-test-cases.json +18 -0
  69. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_increasing-pep.json +18 -0
  70. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_research-sandbagging.json +18 -0
  71. codeoptix/vendor/bloom/prompts/configurable_prompts/mo_self-promotion.json +18 -0
  72. codeoptix/vendor/bloom/prompts/configurable_prompts/sandbag.json +18 -0
  73. codeoptix/vendor/bloom/prompts/configurable_prompts/self-preferential-bias.json +18 -0
  74. codeoptix/vendor/bloom/prompts/configurable_prompts/static-prompts.yaml +72 -0
  75. codeoptix/vendor/bloom/prompts/configurable_prompts/web-search.json +18 -0
  76. codeoptix/vendor/bloom/prompts/step1_understanding.py +63 -0
  77. codeoptix/vendor/bloom/prompts/step2_ideation.py +254 -0
  78. codeoptix/vendor/bloom/prompts/step3_rollout.py +120 -0
  79. codeoptix/vendor/bloom/prompts/step4_judgment.py +183 -0
  80. codeoptix/vendor/bloom/schemas/behavior.schema.json +160 -0
  81. codeoptix/vendor/bloom/schemas/conversation.schema.json +51 -0
  82. codeoptix/vendor/bloom/schemas/transcript_schema.json +2225 -0
  83. codeoptix/vendor/bloom/scripts/step2_ideation.py +667 -0
  84. codeoptix/vendor/bloom/scripts/step4_judgment.py +811 -0
  85. codeoptix/vendor/bloom/transcript_utils.py +440 -0
  86. codeoptix/vendor/bloom/utils.py +700 -0
  87. codeoptix-0.1.3.dist-info/METADATA +295 -0
  88. codeoptix-0.1.3.dist-info/RECORD +92 -0
  89. codeoptix-0.1.3.dist-info/WHEEL +5 -0
  90. codeoptix-0.1.3.dist-info/entry_points.txt +2 -0
  91. codeoptix-0.1.3.dist-info/licenses/LICENSE +203 -0
  92. codeoptix-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,91 @@
1
+ """Configuration management for CodeOptiX."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class LLMConfig(BaseModel):
12
+ """LLM configuration."""
13
+
14
+ provider: str = Field(
15
+ default="anthropic", description="LLM provider (anthropic, openai, google)"
16
+ )
17
+ model: str = Field(default="claude-opus-4-5-20251101", description="Model name")
18
+ api_key: str | None = Field(default=None, description="API key (or use environment variable)")
19
+ temperature: float = Field(default=1.0, description="Temperature for generation")
20
+ max_tokens: int | None = Field(default=None, description="Max tokens")
21
+
22
+
23
+ class AgentConfig(BaseModel):
24
+ """Agent configuration."""
25
+
26
+ name: str = Field(description="Agent name")
27
+ adapter_type: str = Field(description="Adapter type (claude-code, codex, gemini-cli)")
28
+ llm_config: LLMConfig = Field(description="LLM configuration")
29
+ prompt: str | None = Field(default=None, description="Agent prompt/policy")
30
+
31
+
32
+ class BehaviorConfig(BaseModel):
33
+ """Behavior specification configuration."""
34
+
35
+ name: str = Field(description="Behavior name")
36
+ enabled: bool = Field(default=True, description="Whether behavior is enabled")
37
+ severity: str = Field(default="medium", description="Severity level")
38
+ config: dict[str, Any] = Field(default_factory=dict, description="Behavior-specific config")
39
+
40
+
41
+ class CodeOptixConfig(BaseModel):
42
+ """Main CodeOptix configuration."""
43
+
44
+ agent: AgentConfig = Field(description="Agent configuration")
45
+ behaviors: list[BehaviorConfig] = Field(
46
+ default_factory=list, description="Behavior specifications"
47
+ )
48
+ evaluation: dict[str, Any] = Field(default_factory=dict, description="Evaluation settings")
49
+ reflection: dict[str, Any] = Field(default_factory=dict, description="Reflection settings")
50
+ evolution: dict[str, Any] = Field(default_factory=dict, description="Evolution settings")
51
+ artifacts_dir: str = Field(default=".codeoptix/artifacts", description="Artifacts directory")
52
+
53
+
54
+ def load_config(config_path: str | Path) -> CodeOptixConfig:
55
+ """Load configuration from YAML file."""
56
+ config_path = Path(config_path)
57
+
58
+ if not config_path.exists():
59
+ raise FileNotFoundError(f"Config file not found: {config_path}")
60
+
61
+ with open(config_path) as f:
62
+ config_data = yaml.safe_load(f)
63
+
64
+ # Load API keys from environment if not provided
65
+ if "agent" in config_data and "llm_config" in config_data["agent"]:
66
+ llm_config = config_data["agent"]["llm_config"]
67
+ provider = llm_config.get("provider", "anthropic")
68
+
69
+ if not llm_config.get("api_key"):
70
+ # Try to get from environment
71
+ env_key_map = {
72
+ "anthropic": "ANTHROPIC_API_KEY",
73
+ "openai": "OPENAI_API_KEY",
74
+ "google": "GOOGLE_API_KEY",
75
+ }
76
+ env_key = env_key_map.get(provider)
77
+ if env_key:
78
+ api_key = os.getenv(env_key)
79
+ if api_key:
80
+ llm_config["api_key"] = api_key
81
+
82
+ return CodeOptixConfig(**config_data)
83
+
84
+
85
+ def save_config(config: CodeOptixConfig, config_path: str | Path) -> None:
86
+ """Save configuration to YAML file."""
87
+ config_path = Path(config_path)
88
+ config_path.parent.mkdir(parents=True, exist_ok=True)
89
+
90
+ with open(config_path, "w") as f:
91
+ yaml.dump(config.model_dump(), f, default_flow_style=False, sort_keys=False)
codeoptix/utils/llm.py ADDED
@@ -0,0 +1,334 @@
1
+ """LLM client abstraction for multiple providers."""
2
+
3
+ import json
4
+ import os
5
+ import urllib.error
6
+ import urllib.request
7
+ from abc import ABC, abstractmethod
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+ import anthropic
12
+ import openai
13
+ from google import genai
14
+
15
+
16
+ class LLMProvider(str, Enum):
17
+ """Supported LLM providers."""
18
+
19
+ ANTHROPIC = "anthropic"
20
+ OPENAI = "openai"
21
+ GOOGLE = "google"
22
+ OLLAMA = "ollama"
23
+
24
+
25
+ class LLMClient(ABC):
26
+ """Abstract base class for LLM clients."""
27
+
28
+ @abstractmethod
29
+ def chat_completion(
30
+ self,
31
+ messages: list[dict[str, str]],
32
+ model: str,
33
+ temperature: float = 1.0,
34
+ max_tokens: int | None = None,
35
+ **kwargs: Any,
36
+ ) -> str:
37
+ """Generate a chat completion."""
38
+
39
+ @abstractmethod
40
+ def get_available_models(self) -> list[str]:
41
+ """Get list of available models for this provider."""
42
+
43
+
44
+ class AnthropicClient(LLMClient):
45
+ """Anthropic Claude client."""
46
+
47
+ def __init__(self, api_key: str | None = None):
48
+ """Initialize Anthropic client."""
49
+ self.client = anthropic.Anthropic(api_key=api_key)
50
+
51
+ def chat_completion(
52
+ self,
53
+ messages: list[dict[str, str]],
54
+ model: str = "claude-opus-4-5-20251101",
55
+ temperature: float = 1.0,
56
+ max_tokens: int | None = None,
57
+ **kwargs: Any,
58
+ ) -> str:
59
+ """Generate a chat completion using Anthropic."""
60
+ # Convert messages to Anthropic format
61
+ system_message = None
62
+ anthropic_messages = []
63
+
64
+ for msg in messages:
65
+ role = msg.get("role", "user")
66
+ content = msg.get("content", "")
67
+
68
+ if role == "system":
69
+ system_message = content
70
+ elif role == "user":
71
+ anthropic_messages.append({"role": "user", "content": content})
72
+ elif role == "assistant":
73
+ anthropic_messages.append({"role": "assistant", "content": content})
74
+
75
+ response = self.client.messages.create(
76
+ model=model,
77
+ max_tokens=max_tokens or 4096,
78
+ temperature=temperature,
79
+ system=system_message,
80
+ messages=anthropic_messages,
81
+ **kwargs,
82
+ )
83
+
84
+ # Extract text content from response
85
+ if response.content and len(response.content) > 0:
86
+ if hasattr(response.content[0], "text"):
87
+ return response.content[0].text
88
+ return str(response.content[0])
89
+ return ""
90
+
91
+ def get_available_models(self) -> list[str]:
92
+ """Get available Anthropic models."""
93
+ return [
94
+ "claude-opus-4-5-20251101",
95
+ "claude-sonnet-4-5-20251101",
96
+ "claude-haiku-4-5-20251101",
97
+ "claude-3-5-sonnet-20241022",
98
+ "claude-3-5-haiku-20241022",
99
+ ]
100
+
101
+
102
+ class OpenAIClient(LLMClient):
103
+ """OpenAI GPT client."""
104
+
105
+ def __init__(self, api_key: str | None = None):
106
+ """Initialize OpenAI client."""
107
+ self.client = openai.OpenAI(api_key=api_key)
108
+
109
+ def chat_completion(
110
+ self,
111
+ messages: list[dict[str, str]],
112
+ model: str = "gpt-5.2",
113
+ temperature: float = 1.0,
114
+ max_tokens: int | None = None,
115
+ **kwargs: Any,
116
+ ) -> str:
117
+ """Generate a chat completion using OpenAI."""
118
+ response = self.client.chat.completions.create(
119
+ model=model, messages=messages, temperature=temperature, max_tokens=max_tokens, **kwargs
120
+ )
121
+
122
+ if response.choices and len(response.choices) > 0:
123
+ return response.choices[0].message.content or ""
124
+ return ""
125
+
126
+ def get_available_models(self) -> list[str]:
127
+ """Get available OpenAI models."""
128
+ return [
129
+ "gpt-5.2",
130
+ "gpt-4o",
131
+ "gpt-4o-mini",
132
+ "gpt-4-turbo",
133
+ "gpt-4",
134
+ ]
135
+
136
+
137
+ class GoogleClient(LLMClient):
138
+ """Google Gemini client using google-genai SDK."""
139
+
140
+ def __init__(self, api_key: str | None = None):
141
+ """Initialize Google client."""
142
+ # Use the new google-genai Client API
143
+ self.client = genai.Client(api_key=api_key) if api_key else genai.Client()
144
+
145
+ def chat_completion(
146
+ self,
147
+ messages: list[dict[str, str]],
148
+ model: str = "gemini-2.0-flash-exp",
149
+ temperature: float = 1.0,
150
+ max_tokens: int | None = None,
151
+ **kwargs: Any,
152
+ ) -> str:
153
+ """Generate a chat completion using Google Gemini."""
154
+ # Convert messages to the new API format
155
+ # The new API uses Contents format with role and parts
156
+ contents = []
157
+ system_instruction = None
158
+
159
+ for msg in messages:
160
+ role = msg.get("role", "user")
161
+ content = msg.get("content", "")
162
+
163
+ if role == "system":
164
+ system_instruction = content
165
+ else:
166
+ # Create Content object with role and parts
167
+ contents.append({"role": role, "parts": [{"text": content}]})
168
+
169
+ # Build the config
170
+ config_dict = {
171
+ "temperature": temperature,
172
+ }
173
+ if max_tokens:
174
+ config_dict["max_output_tokens"] = max_tokens
175
+ if system_instruction:
176
+ config_dict["system_instruction"] = {"parts": [{"text": system_instruction}]}
177
+
178
+ # Generate content using the new API
179
+ response = self.client.models.generate_content(
180
+ model=model,
181
+ contents=contents,
182
+ config=config_dict,
183
+ )
184
+
185
+ # Extract text from response
186
+ # The new API returns response with text attribute or candidates
187
+ if hasattr(response, "text") and response.text:
188
+ return response.text
189
+ if hasattr(response, "candidates") and response.candidates:
190
+ # Handle structured response with candidates
191
+ candidate = response.candidates[0]
192
+ if hasattr(candidate, "content") and hasattr(candidate.content, "parts"):
193
+ text_parts = []
194
+ for part in candidate.content.parts:
195
+ if hasattr(part, "text") and part.text:
196
+ text_parts.append(part.text)
197
+ if text_parts:
198
+ return "".join(text_parts)
199
+ # Fallback to string representation
200
+ return str(response)
201
+
202
+ def get_available_models(self) -> list[str]:
203
+ """Get available Google models."""
204
+ return [
205
+ "gemini-3-pro",
206
+ "gemini-3-flash",
207
+ "gemini-2.0-flash-exp",
208
+ "gemini-2.5-flash",
209
+ "gemini-2.5-pro",
210
+ "gemini-1.5-pro",
211
+ ]
212
+
213
+
214
+ class OllamaClient(LLMClient):
215
+ """Ollama local model client (http://localhost:11434)."""
216
+
217
+ def __init__(self, api_key: str | None = None, model: str = "llama3.1", **kwargs: Any):
218
+ """Initialize Ollama client.
219
+
220
+ api_key is unused but kept for interface compatibility.
221
+ """
222
+ base = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
223
+ # Normalize: strip trailing slash
224
+ self.base_url = base.rstrip("/")
225
+ # Verify connection on init (best-effort, don't fail if it's down)
226
+ self._verify_connection()
227
+
228
+ def _verify_connection(self) -> None:
229
+ """Verify Ollama connection (best-effort, non-blocking)."""
230
+ try:
231
+ req = urllib.request.Request(
232
+ f"{self.base_url}/api/tags",
233
+ headers={"Content-Type": "application/json"},
234
+ method="GET",
235
+ )
236
+ urllib.request.urlopen(req, timeout=2)
237
+ except Exception:
238
+ # Connection failed, but don't raise - let the actual call handle it
239
+ # This is just a warning check
240
+ pass
241
+
242
+ def chat_completion(
243
+ self,
244
+ messages: list[dict[str, str]],
245
+ model: str = "llama3.1",
246
+ temperature: float = 1.0,
247
+ max_tokens: int | None = None,
248
+ **kwargs: Any,
249
+ ) -> str:
250
+ """Generate a chat completion using a local Ollama model."""
251
+ # Use Ollama's chat API which properly handles chat messages
252
+ payload = {
253
+ "model": model,
254
+ "messages": messages, # Pass messages directly
255
+ "stream": False,
256
+ "options": {
257
+ "temperature": temperature,
258
+ },
259
+ }
260
+ if max_tokens:
261
+ payload["options"]["num_predict"] = max_tokens
262
+
263
+ data = json.dumps(payload).encode("utf-8")
264
+ req = urllib.request.Request(
265
+ f"{self.base_url}/api/chat",
266
+ data=data,
267
+ headers={"Content-Type": "application/json"},
268
+ method="POST",
269
+ )
270
+
271
+ try:
272
+ # Increase timeout for large models like gpt-oss:120b which can take longer
273
+ with urllib.request.urlopen(req, timeout=300) as resp: # 5 minutes for large models
274
+ body = resp.read().decode("utf-8")
275
+ except urllib.error.URLError as exc: # pragma: no cover - network/env specific
276
+ # Provide helpful error message
277
+ default_url = "http://localhost:11434"
278
+ if self.base_url != default_url:
279
+ hint = f" (OLLAMA_BASE_URL is set to {self.base_url}, default is {default_url})"
280
+ else:
281
+ hint = " (default port is 11434)"
282
+ raise RuntimeError(
283
+ f"Failed to contact Ollama at {self.base_url}. "
284
+ f"Is the Ollama daemon running?{hint}\n"
285
+ f" Try: ollama serve\n"
286
+ f" Or set OLLAMA_BASE_URL to the correct URL if using a custom port."
287
+ ) from exc
288
+
289
+ try:
290
+ obj = json.loads(body)
291
+ except json.JSONDecodeError as exc: # pragma: no cover - unexpected response
292
+ raise RuntimeError(f"Invalid JSON from Ollama: {body!r}") from exc
293
+
294
+ # Ollama chat API: response["message"]["content"]
295
+ # Fallback to generate API format for backward compatibility
296
+ if "message" in obj and "content" in obj["message"]:
297
+ return obj["message"]["content"]
298
+ return obj.get("response", "")
299
+
300
+ def get_available_models(self) -> list[str]:
301
+ """Get available Ollama models via /api/tags."""
302
+ req = urllib.request.Request(
303
+ f"{self.base_url}/api/tags",
304
+ headers={"Content-Type": "application/json"},
305
+ method="GET",
306
+ )
307
+ try:
308
+ with urllib.request.urlopen(req) as resp:
309
+ body = resp.read().decode("utf-8")
310
+ obj = json.loads(body)
311
+ except Exception: # pragma: no cover - best-effort helper
312
+ return []
313
+
314
+ models = []
315
+ for m in obj.get("models", []) or []:
316
+ name = m.get("name")
317
+ if isinstance(name, str):
318
+ models.append(name)
319
+ return models
320
+
321
+
322
+ def create_llm_client(
323
+ provider: LLMProvider, api_key: str | None = None, model: str | None = None
324
+ ) -> LLMClient:
325
+ """Factory function to create an LLM client."""
326
+ if provider == LLMProvider.ANTHROPIC:
327
+ return AnthropicClient(api_key=api_key, model=model or "claude-3-5-sonnet-20241022")
328
+ if provider == LLMProvider.OPENAI:
329
+ return OpenAIClient(api_key=api_key, model=model or "gpt-4o")
330
+ if provider == LLMProvider.GOOGLE:
331
+ return GoogleClient(api_key=api_key, model=model or "gemini-1.5-pro")
332
+ if provider == LLMProvider.OLLAMA:
333
+ return OllamaClient(model=model or "llama3.1")
334
+ raise ValueError(f"Unsupported provider: {provider}")
@@ -0,0 +1,133 @@
1
+ """Retry utilities for resilient API calls."""
2
+
3
+ import time
4
+ from collections.abc import Callable
5
+ from functools import wraps
6
+
7
+ from tenacity import (
8
+ retry,
9
+ retry_if_exception_type,
10
+ stop_after_attempt,
11
+ wait_exponential,
12
+ )
13
+
14
+
15
+ class RetryableError(Exception):
16
+ """Base exception for retryable errors."""
17
+
18
+
19
+ class APIError(RetryableError):
20
+ """API-related errors that can be retried."""
21
+
22
+
23
+ class RateLimitError(APIError):
24
+ """Rate limit errors."""
25
+
26
+
27
+ class TimeoutError(APIError):
28
+ """Timeout errors."""
29
+
30
+
31
+ def retry_with_backoff(
32
+ max_attempts: int = 3,
33
+ initial_wait: float = 1.0,
34
+ max_wait: float = 60.0,
35
+ exponential_base: float = 2.0,
36
+ retry_on: tuple[type[Exception], ...] | None = None,
37
+ ):
38
+ """
39
+ Decorator for retrying functions with exponential backoff.
40
+
41
+ Args:
42
+ max_attempts: Maximum number of retry attempts
43
+ initial_wait: Initial wait time in seconds
44
+ max_wait: Maximum wait time in seconds
45
+ exponential_base: Base for exponential backoff
46
+ retry_on: Tuple of exception types to retry on (default: APIError, RateLimitError)
47
+
48
+ Returns:
49
+ Decorated function with retry logic
50
+ """
51
+ if retry_on is None:
52
+ retry_on = (APIError, RateLimitError, TimeoutError)
53
+
54
+ def decorator(func: Callable) -> Callable:
55
+ @wraps(func)
56
+ def wrapper(*args, **kwargs):
57
+ last_exception = None
58
+ wait_time = initial_wait
59
+
60
+ for attempt in range(max_attempts):
61
+ try:
62
+ return func(*args, **kwargs)
63
+ except retry_on as e:
64
+ last_exception = e
65
+ if attempt < max_attempts - 1:
66
+ # Calculate wait time with exponential backoff
67
+ wait_time = min(initial_wait * (exponential_base**attempt), max_wait)
68
+ time.sleep(wait_time)
69
+ else:
70
+ # Last attempt failed
71
+ raise
72
+ except Exception:
73
+ # Non-retryable exception
74
+ raise
75
+
76
+ # Should never reach here, but just in case
77
+ if last_exception:
78
+ raise last_exception
79
+
80
+ return wrapper
81
+
82
+ return decorator
83
+
84
+
85
+ def retry_llm_call(max_attempts: int = 3, initial_wait: float = 1.0, max_wait: float = 60.0):
86
+ """
87
+ Specialized retry decorator for LLM API calls.
88
+
89
+ Handles common LLM API errors:
90
+ - Rate limiting
91
+ - Timeouts
92
+ - Temporary service errors
93
+
94
+ Args:
95
+ max_attempts: Maximum number of retry attempts
96
+ initial_wait: Initial wait time in seconds
97
+ max_wait: Maximum wait time in seconds
98
+
99
+ Returns:
100
+ Decorated function with LLM-specific retry logic
101
+ """
102
+ return retry(
103
+ stop=stop_after_attempt(max_attempts),
104
+ wait=wait_exponential(multiplier=initial_wait, max=max_wait),
105
+ retry=retry_if_exception_type((APIError, RateLimitError, TimeoutError)),
106
+ reraise=True,
107
+ )
108
+
109
+
110
+ def handle_api_error(error: Exception, context: str | None = None) -> str:
111
+ """
112
+ Generate user-friendly error messages from API errors.
113
+
114
+ Args:
115
+ error: The exception that occurred
116
+ context: Optional context about where the error occurred
117
+
118
+ Returns:
119
+ User-friendly error message
120
+ """
121
+ error_type = type(error).__name__
122
+ error_msg = str(error)
123
+
124
+ # Common error patterns
125
+ if "rate limit" in error_msg.lower() or "429" in error_msg:
126
+ return f"Rate limit exceeded. Please wait before retrying. {context or ''}"
127
+ if "timeout" in error_msg.lower() or "timed out" in error_msg.lower():
128
+ return f"Request timed out. The service may be slow. Try again. {context or ''}"
129
+ if "authentication" in error_msg.lower() or "401" in error_msg or "403" in error_msg:
130
+ return f"Authentication failed. Please check your API key. {context or ''}"
131
+ if "quota" in error_msg.lower() or "insufficient" in error_msg.lower():
132
+ return f"API quota exceeded. Please check your account limits. {context or ''}"
133
+ return f"API error ({error_type}): {error_msg}. {context or ''}"
@@ -0,0 +1,2 @@
1
+ """Vendored third-party code for CodeOptiX."""
2
+
@@ -0,0 +1,26 @@
1
+ # Vendored Bloom Framework
2
+
3
+ This directory contains vendored code from the Bloom evaluation framework.
4
+
5
+ ## Source
6
+
7
+ Bloom is an open-source tool for automated behavior evaluation of LLMs. Since it's not available on PyPI, we vendor the necessary components here.
8
+
9
+ ## Files
10
+
11
+ - `utils.py` - Core utility functions for Bloom
12
+ - `globals.py` - Global configuration and model definitions
13
+ - `transcript_utils.py` - Transcript handling utilities
14
+ - `prompts/` - Prompt templates for evaluation stages
15
+ - `orchestrators/` - Orchestration logic for conversations and simulated environments
16
+ - `scripts/` - Evaluation stage scripts (ideation, judgment)
17
+ - `schemas/` - JSON schemas for behaviors and conversations
18
+
19
+ ## Usage
20
+
21
+ The vendored Bloom code is used by CodeOptiX's evaluation engine for scenario generation and behavioral evaluation. It's accessed through the `codeoptix.vendor.bloom` namespace.
22
+
23
+ ## Modifications
24
+
25
+ All imports have been updated to use the `codeoptix.vendor.bloom` namespace instead of relative imports.
26
+
@@ -0,0 +1,11 @@
1
+ """
2
+ Vendored Bloom evaluation framework.
3
+
4
+ Bloom is an open-source tool for automated behavior evaluation of LLMs.
5
+ Since it's not available on PyPI, we vendor the necessary components here.
6
+
7
+ Original source: https://github.com/anthropics/bloom (or similar)
8
+ """
9
+
10
+ __version__ = "0.1.0-vendored"
11
+
@@ -0,0 +1,39 @@
1
+ NUM_RETRIES = 30 # Global variable for the number of retries for API calls
2
+ RETRY_BASE_DELAY = 1.0 # Base delay in seconds for exponential backoff
3
+ RETRY_MAX_DELAY = 60.0 # Maximum delay in seconds
4
+
5
+ # External transcripts directory for wandb runs
6
+ EXTERNAL_TRANSCRIPTS_DIR = "/workspace/transcripts" # Path relative to project root
7
+
8
+ models = {
9
+ # Anthropic
10
+ "claude-sonnet-4.5": {"id": "anthropic/claude-sonnet-4-5-20250929", "org": "anthropic", "name": "Claude Sonnet 4.5"},
11
+ "claude-opus-4.1": {"id": "anthropic/claude-opus-4-1-20250805", "org": "anthropic", "name": "Claude Opus 4.1"},
12
+ "claude-sonnet-4": {"id": "anthropic/claude-sonnet-4-20250514", "org": "anthropic", "name": "Claude Sonnet 4"},
13
+ #"claude-sonnet-3.7": {"id": "anthropic/claude-3-7-sonnet-latest", "org": "anthropic", "name": "Claude Sonnet 3.7"},
14
+ #"claude-opus-4": {"id": "anthropic/claude-opus-4-20250514", "org": "anthropic", "name": "Claude Opus 4"},
15
+ "claude-haiku-4.5": {"id": "anthropic/claude-haiku-4-5-20251001", "org": "anthropic", "name": "Claude Haiku 4.5"},
16
+ # OpenAI
17
+ #"gpt-4.1": {"id": "openai/gpt-4.1", "org": "openai", "name": "GPT-4.1"}, #max tokens: add support
18
+ "gpt-5": {"id": "openai/gpt-5", "org": "openai", "name": "GPT-5"},
19
+ "gpt-5-mini": {"id": "openai/gpt-5-mini", "org": "openai", "name": "GPT-5 Mini"},
20
+ #"gpt-5-nano": {"id": "openai/gpt-5-nano", "org": "openai", "name": "GPT-5 Nano"},
21
+ "gpt-4o": {"id": "openai/gpt-4o", "org": "openai", "name": "GPT-4o"},
22
+ #"gpt-4o-mini": {"id": "openai/gpt-4o-mini", "org": "openai", "name": "GPT-4o Mini"},
23
+ #"o3": {"id": "openai/o3", "org": "openai", "name": "OpenAI o3"},
24
+ "o4-mini": {"id": "openai/o4-mini", "org": "openai", "name": "OpenAI o4-mini"},
25
+ #"gpt-oss-20b": {"id": "openrouter/openai/gpt-oss-20b", "org": "openrouter", "name": "GPT-OSS-20B"},
26
+ "gpt-oss-120b": {"id": "openrouter/openai/gpt-oss-120b", "org": "openrouter", "name": "GPT-OSS-120B"},
27
+ "kimi-k2": {"id": "openrouter/moonshotai/kimi-k2-0905", "org": "openrouter", "name": "Kimi K2"},
28
+ "kimi-k2-thinking": {"id": "openrouter/moonshotai/kimi-k2-thinking", "org": "openrouter", "name": "Kimi K2 Thinking"},
29
+ # Other models via OpenRouter
30
+ #"gemini-2.5-flash": {"id": "openrouter/google/gemini-2.5-flash", "org": "openrouter", "name": "Gemini 2.5 Flash"},
31
+ "gemini-2.5-pro": {"id": "openrouter/google/gemini-2.5-pro", "org": "openrouter", "name": "Gemini 2.5 Pro"},
32
+ "gemini-3-pro-preview": {"id": "openrouter/google/gemini-3-pro-preview", "org": "openrouter"},
33
+
34
+ #"llama-3.1-70b-instruct": {"id": "openrouter/meta-llama/llama-3-70b-instruct", "org": "openrouter", "name": "LLaMA 3.1 70B Instruct"},
35
+ "grok-4": {"id": "openrouter/x-ai/grok-4", "org": "openrouter", "name": "Grok 4"},
36
+
37
+ "deepseek-r1": {"id": "openrouter/deepseek/deepseek-r1", "org": "openrouter", "name": "DeepSeek R1"},
38
+ "deepseek-v3": {"id": "openrouter/deepseek/deepseek-chat-v3-0324", "org": "openrouter"}
39
+ }