codeoptix 0.1.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.
- codeoptix/__init__.py +8 -0
- codeoptix/acp/__init__.py +33 -0
- codeoptix/acp/agent.py +209 -0
- codeoptix/acp/bridge.py +402 -0
- codeoptix/acp/client_adapter.py +312 -0
- codeoptix/acp/code_extractor.py +125 -0
- codeoptix/acp/orchestrator.py +349 -0
- codeoptix/acp/registry.py +294 -0
- codeoptix/adapters/__init__.py +18 -0
- codeoptix/adapters/base.py +50 -0
- codeoptix/adapters/basic.py +195 -0
- codeoptix/adapters/claude_code.py +218 -0
- codeoptix/adapters/codex.py +327 -0
- codeoptix/adapters/factory.py +56 -0
- codeoptix/adapters/gemini_cli.py +370 -0
- codeoptix/artifacts/__init__.py +5 -0
- codeoptix/artifacts/manager.py +193 -0
- codeoptix/behaviors/__init__.py +45 -0
- codeoptix/behaviors/base.py +81 -0
- codeoptix/behaviors/insecure_code.py +129 -0
- codeoptix/behaviors/plan_drift.py +192 -0
- codeoptix/behaviors/vacuous_tests.py +198 -0
- codeoptix/cli.py +1472 -0
- codeoptix/evaluation/__init__.py +23 -0
- codeoptix/evaluation/bloom_integration.py +271 -0
- codeoptix/evaluation/engine.py +274 -0
- codeoptix/evaluation/evaluators.py +308 -0
- codeoptix/evaluation/scenario_generator.py +222 -0
- codeoptix/evolution/__init__.py +7 -0
- codeoptix/evolution/engine.py +206 -0
- codeoptix/evolution/gepa_integration.py +149 -0
- codeoptix/evolution/proposer.py +185 -0
- codeoptix/linters/__init__.py +13 -0
- codeoptix/linters/bandit_linter.py +172 -0
- codeoptix/linters/base.py +105 -0
- codeoptix/linters/coverage_linter.py +156 -0
- codeoptix/linters/flake8_linter.py +156 -0
- codeoptix/linters/html_accessibility_linter.py +374 -0
- codeoptix/linters/language_detector.py +150 -0
- codeoptix/linters/mypy_linter.py +184 -0
- codeoptix/linters/pip_audit_linter.py +152 -0
- codeoptix/linters/pylint_linter.py +198 -0
- codeoptix/linters/ruff_linter.py +206 -0
- codeoptix/linters/runner.py +186 -0
- codeoptix/linters/safety_linter.py +184 -0
- codeoptix/reflection/__init__.py +6 -0
- codeoptix/reflection/engine.py +70 -0
- codeoptix/reflection/generator.py +209 -0
- codeoptix/utils/__init__.py +1 -0
- codeoptix/utils/config.py +91 -0
- codeoptix/utils/llm.py +332 -0
- codeoptix/utils/retry.py +133 -0
- codeoptix/vendor/__init__.py +2 -0
- codeoptix/vendor/bloom/README.md +26 -0
- codeoptix/vendor/bloom/__init__.py +11 -0
- codeoptix/vendor/bloom/globals.py +39 -0
- codeoptix/vendor/bloom/orchestrators/ConversationOrchestrator.py +450 -0
- codeoptix/vendor/bloom/orchestrators/SimEnvOrchestrator.py +839 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/README.md +85 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/default.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/ideation-default.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_animal-welfare.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_contextual-optimism.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defend-objects.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_defer-to-users.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_emotional-bond.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_flattery.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_hardcode-test-cases.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_increasing-pep.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_research-sandbagging.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/mo_self-promotion.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/sandbag.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/self-preferential-bias.json +18 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/static-prompts.yaml +72 -0
- codeoptix/vendor/bloom/prompts/configurable_prompts/web-search.json +18 -0
- codeoptix/vendor/bloom/prompts/step1_understanding.py +63 -0
- codeoptix/vendor/bloom/prompts/step2_ideation.py +254 -0
- codeoptix/vendor/bloom/prompts/step3_rollout.py +120 -0
- codeoptix/vendor/bloom/prompts/step4_judgment.py +183 -0
- codeoptix/vendor/bloom/schemas/behavior.schema.json +160 -0
- codeoptix/vendor/bloom/schemas/conversation.schema.json +51 -0
- codeoptix/vendor/bloom/schemas/transcript_schema.json +2225 -0
- codeoptix/vendor/bloom/scripts/step2_ideation.py +667 -0
- codeoptix/vendor/bloom/scripts/step4_judgment.py +811 -0
- codeoptix/vendor/bloom/transcript_utils.py +440 -0
- codeoptix/vendor/bloom/utils.py +700 -0
- codeoptix-0.1.0.dist-info/METADATA +304 -0
- codeoptix-0.1.0.dist-info/RECORD +91 -0
- codeoptix-0.1.0.dist-info/WHEEL +4 -0
- codeoptix-0.1.0.dist-info/entry_points.txt +2 -0
- codeoptix-0.1.0.dist-info/licenses/LICENSE +203 -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,332 @@
|
|
|
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):
|
|
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
|
+
payload = {
|
|
252
|
+
"model": model,
|
|
253
|
+
"messages": messages,
|
|
254
|
+
"stream": False,
|
|
255
|
+
"options": {
|
|
256
|
+
"temperature": temperature,
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
if max_tokens:
|
|
260
|
+
payload["options"]["num_predict"] = max_tokens
|
|
261
|
+
|
|
262
|
+
data = json.dumps(payload).encode("utf-8")
|
|
263
|
+
req = urllib.request.Request(
|
|
264
|
+
f"{self.base_url}/api/chat",
|
|
265
|
+
data=data,
|
|
266
|
+
headers={"Content-Type": "application/json"},
|
|
267
|
+
method="POST",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
# Increase timeout for large models like gpt-oss:120b which can take longer
|
|
272
|
+
with urllib.request.urlopen(req, timeout=300) as resp: # 5 minutes for large models
|
|
273
|
+
body = resp.read().decode("utf-8")
|
|
274
|
+
except urllib.error.URLError as exc: # pragma: no cover - network/env specific
|
|
275
|
+
# Provide helpful error message
|
|
276
|
+
default_url = "http://localhost:11434"
|
|
277
|
+
if self.base_url != default_url:
|
|
278
|
+
hint = f" (OLLAMA_BASE_URL is set to {self.base_url}, default is {default_url})"
|
|
279
|
+
else:
|
|
280
|
+
hint = " (default port is 11434)"
|
|
281
|
+
raise RuntimeError(
|
|
282
|
+
f"Failed to contact Ollama at {self.base_url}. "
|
|
283
|
+
f"Is the Ollama daemon running?{hint}\n"
|
|
284
|
+
f" Try: ollama serve\n"
|
|
285
|
+
f" Or set OLLAMA_BASE_URL to the correct URL if using a custom port."
|
|
286
|
+
) from exc
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
obj = json.loads(body)
|
|
290
|
+
except json.JSONDecodeError as exc: # pragma: no cover - unexpected response
|
|
291
|
+
raise RuntimeError(f"Invalid JSON from Ollama: {body!r}") from exc
|
|
292
|
+
|
|
293
|
+
# Ollama chat API: response["message"]["content"]
|
|
294
|
+
message = obj.get("message") or {}
|
|
295
|
+
content = message.get("content")
|
|
296
|
+
if isinstance(content, str):
|
|
297
|
+
return content
|
|
298
|
+
return str(content) if content is not None else ""
|
|
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(provider: LLMProvider, api_key: str | None = None) -> LLMClient:
|
|
323
|
+
"""Factory function to create an LLM client."""
|
|
324
|
+
if provider == LLMProvider.ANTHROPIC:
|
|
325
|
+
return AnthropicClient(api_key=api_key)
|
|
326
|
+
if provider == LLMProvider.OPENAI:
|
|
327
|
+
return OpenAIClient(api_key=api_key)
|
|
328
|
+
if provider == LLMProvider.GOOGLE:
|
|
329
|
+
return GoogleClient(api_key=api_key)
|
|
330
|
+
if provider == LLMProvider.OLLAMA:
|
|
331
|
+
return OllamaClient(api_key=api_key)
|
|
332
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
codeoptix/utils/retry.py
ADDED
|
@@ -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,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
|
+
}
|