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,50 @@
1
+ """Base adapter interface for coding agents."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+
8
+ @dataclass
9
+ class AgentOutput:
10
+ """Standardized output from any coding agent."""
11
+
12
+ code: str
13
+ tests: str | None = None
14
+ traces: list[dict[str, Any]] | None = None
15
+ metadata: dict[str, Any] | None = None
16
+ prompt_used: str | None = None
17
+
18
+
19
+ class AgentAdapter(ABC):
20
+ """Base interface for agent adapters."""
21
+
22
+ def __init__(self, config: dict[str, Any]):
23
+ """Initialize adapter with configuration."""
24
+ self.config = config
25
+ self._current_prompt: str | None = None
26
+
27
+ @abstractmethod
28
+ def execute(self, prompt: str, context: dict[str, Any] | None = None) -> AgentOutput:
29
+ """
30
+ Execute agent with prompt and return standardized output.
31
+
32
+ Args:
33
+ prompt: The task prompt for the agent
34
+ context: Optional context (files, workspace info, etc.)
35
+
36
+ Returns:
37
+ AgentOutput with code, tests, traces, and metadata
38
+ """
39
+
40
+ @abstractmethod
41
+ def get_prompt(self) -> str:
42
+ """Get current agent prompt/policy."""
43
+
44
+ @abstractmethod
45
+ def update_prompt(self, new_prompt: str) -> None:
46
+ """Update agent prompt/policy."""
47
+
48
+ @abstractmethod
49
+ def get_adapter_type(self) -> str:
50
+ """Get the type identifier for this adapter."""
@@ -0,0 +1,195 @@
1
+ """Basic agent adapter for testing and simple use cases."""
2
+
3
+ from typing import Any
4
+
5
+ from codeoptix.adapters.base import AgentAdapter, AgentOutput
6
+ from codeoptix.utils.llm import LLMClient
7
+
8
+
9
+ class BasicAdapter(AgentAdapter):
10
+ """
11
+ Basic agent adapter that works with any LLM provider.
12
+
13
+ This adapter doesn't require any external agent software and can be used
14
+ for testing or simple evaluation scenarios. It uses the LLM directly
15
+ with a simple coding assistant prompt.
16
+ """
17
+
18
+ def __init__(self, config: dict[str, Any]):
19
+ """Initialize basic adapter."""
20
+ super().__init__(config)
21
+
22
+ # Get LLM configuration
23
+ llm_config = config.get("llm_config", {})
24
+ if not llm_config:
25
+ raise ValueError("BasicAdapter requires 'llm_config' in configuration")
26
+
27
+ # Create LLM client
28
+ from codeoptix.utils.llm import LLMProvider, create_llm_client
29
+
30
+ provider_name = llm_config.get("provider", "ollama")
31
+ self.llm_client: LLMClient = create_llm_client(
32
+ LLMProvider(provider_name), llm_config.get("api_key"), llm_config.get("model")
33
+ )
34
+
35
+ # Set model
36
+ self.model = llm_config.get("model", "llama3.2:3b")
37
+
38
+ # Set default prompt
39
+ self._current_prompt = config.get("prompt") or self._get_default_prompt()
40
+
41
+ def get_adapter_type(self) -> str:
42
+ """Get adapter type."""
43
+ return "basic"
44
+
45
+ def _get_default_prompt(self) -> str:
46
+ """Get default basic coding assistant prompt."""
47
+ return """You are a helpful coding assistant. Your task is to write clean, secure, and well-tested code.
48
+
49
+ Guidelines:
50
+ - Write secure code: validate inputs, avoid hardcoded secrets, use proper error handling
51
+ - Write comprehensive tests: cover edge cases, use meaningful assertions
52
+ - Follow coding best practices: clear variable names, proper structure, documentation
53
+ - Consider the user's requirements and context provided
54
+
55
+ When given a coding task, provide:
56
+ 1. Well-structured, readable code
57
+ 2. Appropriate tests for the code
58
+ 3. Brief explanation of the implementation"""
59
+
60
+ def get_prompt(self) -> str:
61
+ """Get current system prompt."""
62
+ return self._current_prompt or self._get_default_prompt()
63
+
64
+ def update_prompt(self, new_prompt: str) -> None:
65
+ """Update the system prompt."""
66
+ self._current_prompt = new_prompt
67
+
68
+ def execute(self, prompt: str, context: dict[str, Any] | None = None) -> "AgentOutput":
69
+ """
70
+ Execute a coding task using the LLM directly.
71
+
72
+ Args:
73
+ prompt: The coding task prompt
74
+ context: Optional context information
75
+
76
+ Returns:
77
+ AgentOutput with generated code and tests
78
+ """
79
+ from codeoptix.adapters.base import AgentOutput
80
+
81
+ context = context or {}
82
+
83
+ # Build the full prompt
84
+ full_prompt = self._build_full_prompt(prompt, context)
85
+
86
+ # Get response from LLM
87
+ messages = [
88
+ {"role": "system", "content": self._current_prompt},
89
+ {"role": "user", "content": full_prompt},
90
+ ]
91
+
92
+ response = self.llm_client.chat_completion(
93
+ messages=messages, model=self.model, temperature=0.7, max_tokens=2048
94
+ )
95
+
96
+ # Parse the response into code and tests
97
+ code, tests = self._parse_response(response)
98
+
99
+ return AgentOutput(
100
+ code=code,
101
+ tests=tests,
102
+ prompt_used=self._current_prompt,
103
+ metadata={"model": self.model, "adapter_type": "basic", "full_response": response},
104
+ )
105
+
106
+ def _build_full_prompt(self, prompt: str, context: dict[str, Any]) -> str:
107
+ """Build the full prompt including context."""
108
+ parts = []
109
+
110
+ # Add context if provided
111
+ if context.get("plan"):
112
+ parts.append(f"Plan/Requirements: {context['plan']}")
113
+ if context.get("existing_code"):
114
+ parts.append(f"Existing Code:\n{context['existing_code']}")
115
+ if context.get("requirements"):
116
+ parts.append(f"Requirements: {context['requirements']}")
117
+
118
+ # Add the main task
119
+ parts.append(f"Task: {prompt}")
120
+
121
+ # Add output format instructions
122
+ parts.append("""
123
+ Please provide your response in the following format:
124
+
125
+ CODE:
126
+ ```python
127
+ # Your code here - must be valid Python
128
+ ```
129
+
130
+ TESTS:
131
+ ```python
132
+ # Your tests here - must be valid Python
133
+ ```
134
+
135
+ EXPLANATION:
136
+ Brief explanation of your implementation.
137
+ """)
138
+
139
+ return "\n\n".join(parts)
140
+
141
+ def _parse_response(self, response: str) -> tuple[str, str]:
142
+ """Parse LLM response into code and tests."""
143
+ code = ""
144
+ tests = ""
145
+
146
+ # Simple parsing - look for CODE and TESTS sections
147
+ lines = response.split("\n")
148
+ current_section = None
149
+ code_lines = []
150
+ test_lines = []
151
+
152
+ for line in lines:
153
+ line_lower = line.lower().strip()
154
+ if line_lower.startswith("code:") or "```" in line_lower:
155
+ current_section = "code"
156
+ continue
157
+ if line_lower.startswith(("tests:", "test:")):
158
+ current_section = "tests"
159
+ continue
160
+ if current_section == "code" and line.strip():
161
+ # Remove markdown code blocks
162
+ if "```" in line:
163
+ continue
164
+ code_lines.append(line)
165
+ elif current_section == "tests" and line.strip():
166
+ # Remove markdown code blocks
167
+ if "```" in line:
168
+ continue
169
+ test_lines.append(line)
170
+
171
+ # If no clear sections found, try to extract from the whole response
172
+ if not code_lines and not test_lines:
173
+ # Look for function definitions for code
174
+ # Look for test functions for tests
175
+ for line in lines:
176
+ if line.strip().startswith("def ") and "test" in line.lower():
177
+ current_section = "tests"
178
+ test_lines.append(line)
179
+ elif line.strip().startswith("def ") and current_section != "tests":
180
+ current_section = "code"
181
+ code_lines.append(line)
182
+ elif current_section == "code":
183
+ code_lines.append(line)
184
+ elif current_section == "tests":
185
+ test_lines.append(line)
186
+
187
+ code = "\n".join(code_lines).strip()
188
+ tests = "\n".join(test_lines).strip()
189
+
190
+ # Fallback if parsing failed
191
+ if not code and not tests:
192
+ # Assume the entire response is code
193
+ code = response.strip()
194
+
195
+ return code, tests
@@ -0,0 +1,221 @@
1
+ """Claude Code adapter for CodeOptix.
2
+
3
+ This adapter interfaces with Claude Code by using the Anthropic API directly,
4
+ which is what Claude Code uses under the hood. Since Claude Code is not open source,
5
+ we use the Anthropic SDK to match the same API patterns.
6
+
7
+ Note: Claude Code is a full CLI tool with plugins, hooks, and agents. This adapter
8
+ provides a simplified interface for CodeOptix's evaluation framework using the
9
+ same Anthropic API that Claude Code uses.
10
+ """
11
+
12
+ import os
13
+ from typing import Any
14
+
15
+ from codeoptix.adapters.base import AgentAdapter, AgentOutput
16
+ from codeoptix.utils.llm import LLMProvider, create_llm_client
17
+
18
+
19
+ class ClaudeCodeAdapter(AgentAdapter):
20
+ """
21
+ Adapter for Claude Code (Anthropic's coding agent).
22
+
23
+ Claude Code uses the Anthropic Messages API. This adapter uses the same
24
+ API directly to match Claude Code's behavior. The adapter follows the
25
+ patterns from the Anthropic Python SDK reference implementation.
26
+
27
+ Since Claude Code is not open source, we use the Anthropic API directly,
28
+ which is the same API that Claude Code uses internally.
29
+ """
30
+
31
+ def __init__(self, config: dict[str, Any]):
32
+ """Initialize Claude Code adapter."""
33
+ super().__init__(config)
34
+
35
+ # Get LLM configuration
36
+ llm_config = config.get("llm_config", {})
37
+ # Respect provider from config (defaults to ANTHROPIC for backward compatibility)
38
+ provider_name = llm_config.get("provider", "anthropic").upper()
39
+ try:
40
+ provider = LLMProvider[provider_name]
41
+ except KeyError as e:
42
+ supported_providers = ", ".join([p.value for p in LLMProvider])
43
+ raise ValueError(
44
+ f"Invalid LLM provider '{provider_name}'. "
45
+ f"Supported providers: {supported_providers}"
46
+ ) from e
47
+
48
+ # Get API key (not required for Ollama)
49
+ api_key = None
50
+ if provider != LLMProvider.OLLAMA:
51
+ api_key = (
52
+ llm_config.get("api_key")
53
+ or os.getenv(f"{provider_name}_API_KEY")
54
+ or os.getenv("ANTHROPIC_API_KEY")
55
+ )
56
+
57
+ self.llm_client = create_llm_client(provider, api_key=api_key)
58
+ # Use model from config, or default based on provider
59
+ if provider == LLMProvider.OLLAMA:
60
+ self.model = llm_config.get("model", "llama3.1:8b")
61
+ else:
62
+ # Use current Claude Code default model (claude-opus-4-5-20251101)
63
+ self.model = llm_config.get("model", "claude-opus-4-5-20251101")
64
+ self.temperature = llm_config.get("temperature", 1.0)
65
+ self.max_tokens = llm_config.get("max_tokens", 4096)
66
+
67
+ # Get initial prompt if provided
68
+ self._current_prompt = config.get("prompt") or self._get_default_prompt()
69
+
70
+ def _get_default_prompt(self) -> str:
71
+ """Get default Claude Code system prompt.
72
+
73
+ This matches the style of prompts used in Claude Code plugins and
74
+ follows Claude Code's best practices for coding assistants.
75
+ """
76
+ return """You are a helpful coding assistant. You write clean, secure, and well-tested code.
77
+ Follow best practices:
78
+ - Write secure code (no hardcoded secrets, validate inputs)
79
+ - Write meaningful tests
80
+ - Follow the user's requirements and planning artifacts
81
+ - Use appropriate error handling
82
+ - Write clear, maintainable code"""
83
+
84
+ def execute(self, prompt: str, context: dict[str, Any] | None = None) -> AgentOutput:
85
+ """
86
+ Execute Claude Code with a task prompt.
87
+
88
+ Uses the Anthropic Messages API (same API that Claude Code uses) to
89
+ generate code and test responses. Follows the patterns from the
90
+ Anthropic Python SDK reference implementation.
91
+ """
92
+ context = context or {}
93
+
94
+ # Build messages for Claude (following Anthropic SDK pattern)
95
+ messages = []
96
+
97
+ # Add system prompt (Anthropic API uses separate system parameter)
98
+ system_prompt = self._current_prompt if self._current_prompt else None
99
+
100
+ # Add context if provided
101
+ user_prompt = prompt
102
+ if context:
103
+ context_str = self._format_context(context)
104
+ user_prompt = f"{context_str}\n\nTask: {prompt}"
105
+
106
+ messages.append({"role": "user", "content": user_prompt})
107
+
108
+ # Get response from Claude using Anthropic API
109
+ try:
110
+ # Use the LLM client which wraps Anthropic SDK
111
+ # The client handles system message separately (as per Anthropic API)
112
+ if system_prompt:
113
+ messages.insert(0, {"role": "system", "content": system_prompt})
114
+
115
+ response = self.llm_client.chat_completion(
116
+ messages=messages,
117
+ model=self.model,
118
+ temperature=self.temperature,
119
+ max_tokens=self.max_tokens,
120
+ )
121
+
122
+ # Extract code from response
123
+ code, tests = self._extract_code_and_tests(response)
124
+
125
+ return AgentOutput(
126
+ code=code,
127
+ tests=tests,
128
+ traces=[{"type": "llm_response", "content": response}],
129
+ metadata={
130
+ "model": self.model,
131
+ "temperature": self.temperature,
132
+ "provider": "anthropic",
133
+ "api": "messages", # Anthropic Messages API
134
+ },
135
+ prompt_used=self._current_prompt,
136
+ )
137
+ except Exception as e:
138
+ # Return error output
139
+ return AgentOutput(
140
+ code="",
141
+ tests=None,
142
+ traces=[{"type": "error", "error": str(e)}],
143
+ metadata={"error": str(e)},
144
+ prompt_used=self._current_prompt,
145
+ )
146
+
147
+ def _format_context(self, context: dict[str, Any]) -> str:
148
+ """Format context for inclusion in prompt."""
149
+ parts = []
150
+
151
+ if "files" in context:
152
+ parts.append("Files:")
153
+ for file_path, content in context["files"].items():
154
+ parts.append(f"\n{file_path}:\n{content}")
155
+
156
+ if "workspace" in context:
157
+ parts.append(f"\nWorkspace: {context['workspace']}")
158
+
159
+ return "\n".join(parts)
160
+
161
+ def _extract_code_and_tests(self, response: str) -> tuple[str, str | None]:
162
+ """
163
+ Extract code and tests from Claude response.
164
+
165
+ Claude Code typically returns code in markdown code blocks. This method
166
+ extracts Python code blocks and identifies test files/patterns.
167
+ """
168
+ import re
169
+
170
+ code = ""
171
+ tests = None
172
+
173
+ # Find all code blocks (support various language tags)
174
+ # Pattern matches: ```python, ```py, ```, etc.
175
+ code_blocks = re.findall(r"```(?:python|py)?\n(.*?)```", response, re.DOTALL)
176
+
177
+ if code_blocks:
178
+ # First block is usually the main code
179
+ code = code_blocks[0].strip()
180
+
181
+ # Look for test files or test blocks
182
+ # Pattern 1: Explicit test file mentions
183
+ test_file_pattern = r"(?:test|tests?)[^`]*?\.py.*?```(?:python|py)?\n(.*?)```"
184
+ # Pattern 2: Test function definitions
185
+ test_function_pattern = r"def\s+test_.*?```(?:python|py)?\n(.*?)```"
186
+ # Pattern 3: Test class definitions
187
+ test_class_pattern = r"class\s+Test.*?```(?:python|py)?\n(.*?)```"
188
+
189
+ test_patterns = [test_file_pattern, test_function_pattern, test_class_pattern]
190
+
191
+ test_matches = []
192
+ for pattern in test_patterns:
193
+ matches = re.findall(pattern, response, re.DOTALL | re.IGNORECASE)
194
+ if matches:
195
+ test_matches.extend(matches)
196
+
197
+ if test_matches:
198
+ tests = "\n\n".join(test_matches)
199
+
200
+ # If no code blocks found, check if entire response is code
201
+ if not code:
202
+ # Check if response looks like code (no markdown, mostly code-like)
203
+ if not response.strip().startswith("#") and "\n" in response:
204
+ # Might be raw code without markdown
205
+ code = response.strip()
206
+ else:
207
+ code = response
208
+
209
+ return code, tests
210
+
211
+ def get_prompt(self) -> str:
212
+ """Get current Claude Code prompt."""
213
+ return self._current_prompt or ""
214
+
215
+ def update_prompt(self, new_prompt: str) -> None:
216
+ """Update Claude Code prompt."""
217
+ self._current_prompt = new_prompt
218
+
219
+ def get_adapter_type(self) -> str:
220
+ """Get adapter type identifier."""
221
+ return "claude-code"