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,327 @@
1
+ """Codex (OpenAI) adapter for CodeOptix.
2
+
3
+ This adapter interfaces with OpenAI Codex CLI, which uses the Responses API
4
+ for agent-based code generation and execution. The adapter follows the pattern
5
+ used in the official Codex TypeScript SDK, executing the CLI via subprocess
6
+ and parsing JSONL output.
7
+
8
+ Note: Codex is a full CLI tool that executes code in sandboxes. This adapter
9
+ provides a simplified interface for CodeOptix's evaluation framework.
10
+ """
11
+
12
+ import json
13
+ import os
14
+ import subprocess
15
+ from typing import Any
16
+
17
+ from codeoptix.adapters.base import AgentAdapter, AgentOutput
18
+
19
+
20
+ class CodexAdapter(AgentAdapter):
21
+ """
22
+ Adapter for OpenAI Codex CLI.
23
+
24
+ Codex is a coding agent that uses OpenAI's Responses API. This adapter
25
+ executes Codex via the CLI (similar to the TypeScript SDK) and extracts
26
+ code and test outputs from the agent's responses.
27
+
28
+ The adapter uses `codex exec` in non-interactive mode with JSON output
29
+ to capture structured responses from the agent.
30
+ """
31
+
32
+ def __init__(self, config: dict[str, Any]):
33
+ """Initialize Codex adapter."""
34
+ super().__init__(config)
35
+
36
+ # Get LLM configuration
37
+ llm_config = config.get("llm_config", {})
38
+ self.api_key = (
39
+ llm_config.get("api_key") or os.getenv("OPENAI_API_KEY") or os.getenv("CODEX_API_KEY")
40
+ )
41
+ self.model = llm_config.get("model", "gpt-4o")
42
+ self.base_url = llm_config.get("base_url")
43
+
44
+ # Codex CLI path (defaults to system PATH)
45
+ self.codex_path = config.get("codex_path") or self._find_codex_path()
46
+
47
+ # Working directory for Codex execution
48
+ self.working_directory = config.get("working_directory") or os.getcwd()
49
+
50
+ # Sandbox mode (read-only by default for safety)
51
+ self.sandbox_mode = config.get("sandbox_mode", "read-only")
52
+
53
+ # Get initial prompt if provided
54
+ self._current_prompt = config.get("prompt") or self._get_default_prompt()
55
+
56
+ def _find_codex_path(self) -> str:
57
+ """Find Codex CLI executable path."""
58
+ # First check if codex is in PATH
59
+ import shutil
60
+
61
+ codex_path = shutil.which("codex")
62
+ if codex_path:
63
+ return codex_path
64
+
65
+ # Fallback: try common installation locations
66
+ possible_paths = [
67
+ os.path.expanduser("~/.local/bin/codex"),
68
+ "/usr/local/bin/codex",
69
+ "/opt/homebrew/bin/codex", # macOS Homebrew
70
+ ]
71
+
72
+ for path in possible_paths:
73
+ if os.path.exists(path) and os.access(path, os.X_OK):
74
+ return path
75
+
76
+ # If not found, return "codex" and let subprocess handle the error
77
+ return "codex"
78
+
79
+ def _get_default_prompt(self) -> str:
80
+ """Get default Codex system prompt."""
81
+ return """You are an expert Python programmer. Write clean, secure, and well-tested code.
82
+ Best practices:
83
+ - No hardcoded secrets or credentials
84
+ - Write comprehensive tests
85
+ - Follow specifications exactly"""
86
+
87
+ def execute(self, prompt: str, context: dict[str, Any] | None = None) -> AgentOutput:
88
+ """
89
+ Execute Codex with a task prompt.
90
+
91
+ Uses `codex exec` in non-interactive mode with JSON output to capture
92
+ the agent's responses, code generation, and test execution.
93
+ """
94
+ context = context or {}
95
+
96
+ # Build the full prompt with context
97
+ full_prompt = self._build_prompt(prompt, context)
98
+
99
+ # Execute Codex CLI
100
+ try:
101
+ result = self._execute_codex_cli(full_prompt)
102
+
103
+ # Parse JSONL output to extract code and tests
104
+ code, tests = self._extract_from_jsonl(result)
105
+
106
+ return AgentOutput(
107
+ code=code,
108
+ tests=tests,
109
+ traces=result.get("traces", []),
110
+ metadata={
111
+ "model": self.model,
112
+ "provider": "openai",
113
+ "codex_version": result.get("codex_version"),
114
+ "usage": result.get("usage"),
115
+ },
116
+ prompt_used=self._current_prompt,
117
+ )
118
+ except Exception as e:
119
+ return AgentOutput(
120
+ code="",
121
+ tests=None,
122
+ traces=[{"type": "error", "error": str(e)}],
123
+ metadata={"error": str(e)},
124
+ prompt_used=self._current_prompt,
125
+ )
126
+
127
+ def _build_prompt(self, prompt: str, context: dict[str, Any]) -> str:
128
+ """Build the full prompt including system instructions and context."""
129
+ parts = []
130
+
131
+ # Add system prompt if available
132
+ if self._current_prompt:
133
+ parts.append(f"System instructions: {self._current_prompt}\n")
134
+
135
+ # Add context
136
+ if context:
137
+ if "plan" in context:
138
+ parts.append(f"Plan: {context['plan']}\n")
139
+ if "requirements" in context:
140
+ reqs = context["requirements"]
141
+ if isinstance(reqs, list):
142
+ reqs = "\n".join(f"- {r}" for r in reqs)
143
+ parts.append(f"Requirements:\n{reqs}\n")
144
+ if "files" in context:
145
+ parts.append("Files:")
146
+ for file_path, content in context["files"].items():
147
+ parts.append(f"\n{file_path}:\n{content}")
148
+
149
+ # Add the main task
150
+ parts.append(f"\nTask: {prompt}")
151
+
152
+ return "\n".join(parts)
153
+
154
+ def _execute_codex_cli(self, prompt: str) -> dict[str, Any]:
155
+ """
156
+ Execute Codex CLI via subprocess and parse JSONL output.
157
+
158
+ This follows the pattern from the TypeScript SDK, using `codex exec`
159
+ with `--experimental-json` flag to get structured output.
160
+ """
161
+ # Build command arguments
162
+ cmd = [self.codex_path, "exec", "--experimental-json"]
163
+
164
+ # Add model if specified
165
+ if self.model:
166
+ cmd.extend(["--model", self.model])
167
+
168
+ # Add sandbox mode
169
+ if self.sandbox_mode:
170
+ cmd.extend(["--sandbox", self.sandbox_mode])
171
+
172
+ # Add working directory
173
+ if self.working_directory:
174
+ cmd.extend(["--cd", self.working_directory])
175
+
176
+ # Skip git repo check for evaluation contexts
177
+ cmd.append("--skip-git-repo-check")
178
+
179
+ # Set up environment
180
+ env = os.environ.copy()
181
+ if self.api_key:
182
+ env["CODEX_API_KEY"] = self.api_key
183
+ if self.base_url:
184
+ env["OPENAI_BASE_URL"] = self.base_url
185
+
186
+ # Execute and capture output
187
+ try:
188
+ process = subprocess.Popen(
189
+ cmd,
190
+ stdin=subprocess.PIPE,
191
+ stdout=subprocess.PIPE,
192
+ stderr=subprocess.PIPE,
193
+ text=True,
194
+ env=env,
195
+ cwd=self.working_directory,
196
+ )
197
+
198
+ stdout, stderr = process.communicate(input=prompt, timeout=300)
199
+
200
+ if process.returncode != 0:
201
+ raise RuntimeError(f"Codex CLI exited with code {process.returncode}: {stderr}")
202
+
203
+ # Parse JSONL output
204
+ return self._parse_jsonl_output(stdout, stderr)
205
+
206
+ except subprocess.TimeoutExpired:
207
+ process.kill()
208
+ raise RuntimeError("Codex CLI execution timed out after 300 seconds")
209
+ except FileNotFoundError:
210
+ raise RuntimeError(
211
+ f"Codex CLI not found at '{self.codex_path}'. "
212
+ "Please install Codex CLI: npm install -g @openai/codex"
213
+ )
214
+
215
+ def _parse_jsonl_output(self, stdout: str, stderr: str) -> dict[str, Any]:
216
+ """
217
+ Parse JSONL output from Codex CLI.
218
+
219
+ Codex outputs events as JSON Lines (JSONL), including:
220
+ - thread.started
221
+ - turn.started/turn.completed
222
+ - item.completed (agent_message, command_execution, file_change, etc.)
223
+ """
224
+ traces = []
225
+ items = []
226
+ usage = None
227
+ final_message = None
228
+
229
+ # Parse each JSON line
230
+ for line in stdout.strip().split("\n"):
231
+ if not line.strip():
232
+ continue
233
+
234
+ try:
235
+ event = json.loads(line)
236
+ event_type = event.get("type", "")
237
+
238
+ traces.append(event)
239
+
240
+ if event_type == "turn.completed":
241
+ usage = event.get("usage")
242
+ elif event_type == "item.completed":
243
+ item = event.get("item", {})
244
+ items.append(item)
245
+
246
+ # Extract final agent message
247
+ if item.get("type") == "agent_message":
248
+ final_message = item.get("text", "")
249
+
250
+ except json.JSONDecodeError:
251
+ # Skip invalid JSON lines
252
+ continue
253
+
254
+ return {
255
+ "traces": traces,
256
+ "items": items,
257
+ "usage": usage,
258
+ "final_message": final_message,
259
+ "stderr": stderr,
260
+ }
261
+
262
+ def _extract_from_jsonl(self, result: dict[str, Any]) -> tuple[str, str | None]:
263
+ """
264
+ Extract code and tests from Codex JSONL output.
265
+
266
+ Looks for:
267
+ - file_change items (code files created/modified)
268
+ - command_execution items (test runs)
269
+ - agent_message items (code in markdown blocks)
270
+ """
271
+ items = result.get("items", [])
272
+ code = ""
273
+ tests = None
274
+
275
+ # Look for file changes (code files)
276
+ code_files = []
277
+ test_files = []
278
+
279
+ for item in items:
280
+ item_type = item.get("type")
281
+
282
+ if item_type == "file_change":
283
+ file_path = item.get("file_path", "")
284
+ content = item.get("content", "")
285
+
286
+ if file_path.endswith(".py"):
287
+ if "test" in file_path.lower() or "test_" in file_path:
288
+ test_files.append((file_path, content))
289
+ else:
290
+ code_files.append((file_path, content))
291
+
292
+ elif item_type == "agent_message":
293
+ # Extract code from markdown code blocks in messages
294
+ text = item.get("text", "")
295
+ import re
296
+
297
+ code_blocks = re.findall(r"```(?:python)?\n(.*?)```", text, re.DOTALL)
298
+ if code_blocks:
299
+ code = code_blocks[0].strip()
300
+
301
+ # Use file changes if available, otherwise use extracted code
302
+ if code_files:
303
+ # Combine all code files
304
+ code = "\n\n".join(f"# {path}\n{content}" for path, content in code_files)
305
+ elif not code:
306
+ # Fallback to final message
307
+ final_message = result.get("final_message", "")
308
+ if final_message:
309
+ code = final_message
310
+
311
+ # Extract tests
312
+ if test_files:
313
+ tests = "\n\n".join(f"# {path}\n{content}" for path, content in test_files)
314
+
315
+ return code, tests
316
+
317
+ def get_prompt(self) -> str:
318
+ """Get current Codex prompt."""
319
+ return self._current_prompt or ""
320
+
321
+ def update_prompt(self, new_prompt: str) -> None:
322
+ """Update Codex prompt."""
323
+ self._current_prompt = new_prompt
324
+
325
+ def get_adapter_type(self) -> str:
326
+ """Get adapter type identifier."""
327
+ return "codex"
@@ -0,0 +1,56 @@
1
+ """Factory for creating agent adapters."""
2
+
3
+ from typing import Any
4
+
5
+ from codeoptix.adapters.base import AgentAdapter
6
+ from codeoptix.adapters.basic import BasicAdapter
7
+ from codeoptix.adapters.claude_code import ClaudeCodeAdapter
8
+ from codeoptix.adapters.codex import CodexAdapter
9
+ from codeoptix.adapters.gemini_cli import GeminiCLIAdapter
10
+
11
+
12
+ def create_adapter(adapter_type: str, config: dict[str, Any]) -> AgentAdapter:
13
+ """
14
+ Factory function to create an agent adapter.
15
+
16
+ Args:
17
+ adapter_type: Type of adapter ("basic", "claude-code", "codex", "gemini-cli")
18
+ config: Configuration dictionary for the adapter
19
+
20
+ Returns:
21
+ AgentAdapter instance
22
+
23
+ Raises:
24
+ ValueError: If adapter_type is not supported
25
+ """
26
+ adapter_map = {
27
+ "basic": BasicAdapter,
28
+ "claude-code": ClaudeCodeAdapter,
29
+ "codex": CodexAdapter,
30
+ "gemini-cli": GeminiCLIAdapter,
31
+ }
32
+
33
+ adapter_class = adapter_map.get(adapter_type)
34
+ if adapter_class is None:
35
+ supported = ", ".join(adapter_map.keys())
36
+ raise ValueError(
37
+ f"Unsupported adapter type: '{adapter_type}'. "
38
+ f"Supported types: {supported}. "
39
+ f"For testing without external agents, use 'basic'."
40
+ )
41
+
42
+ try:
43
+ return adapter_class(config)
44
+ except KeyError as e:
45
+ if "api_key" in str(e) or "llm_config" in str(e):
46
+ raise ValueError(
47
+ f"Missing required configuration for {adapter_type}. "
48
+ f"Please provide 'llm_config' with 'api_key' in the adapter configuration. "
49
+ f"Example: {{'llm_config': {{'provider': 'openai', 'api_key': 'your-key'}}}}"
50
+ ) from e
51
+ raise
52
+ except Exception as e:
53
+ raise ValueError(
54
+ f"Failed to initialize {adapter_type} adapter: {e}. "
55
+ f"Please check your configuration and API keys."
56
+ ) from e