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,312 @@
1
+ """ACP Client Adapter for connecting to other agents via ACP.
2
+
3
+ This allows CodeOptiX to connect to other ACP-compatible agents (Claude Code, Codex, etc.)
4
+ and perform quality engineering on their outputs.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from acp import (
13
+ PROTOCOL_VERSION,
14
+ Client,
15
+ RequestError,
16
+ connect_to_agent,
17
+ text_block,
18
+ )
19
+ from acp.core import ClientSideConnection
20
+ from acp.schema import (
21
+ AgentMessageChunk,
22
+ ClientCapabilities,
23
+ CreateTerminalResponse,
24
+ Implementation,
25
+ ReadTextFileResponse,
26
+ RequestPermissionResponse,
27
+ WriteTextFileResponse,
28
+ )
29
+
30
+ from codeoptix.acp.code_extractor import extract_all_code, extract_code_from_text
31
+ from codeoptix.adapters.base import AgentAdapter, AgentOutput
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class ACPClientAdapter(AgentAdapter):
37
+ """Adapter for connecting to ACP-compatible agents."""
38
+
39
+ def __init__(
40
+ self,
41
+ agent_command: list[str],
42
+ cwd: str | None = None,
43
+ env: dict[str, str] | None = None,
44
+ ):
45
+ """Initialize ACP client adapter.
46
+
47
+ Args:
48
+ agent_command: Command to spawn the ACP agent (e.g., ["python", "agent.py"])
49
+ cwd: Working directory for the agent
50
+ env: Environment variables for the agent
51
+ """
52
+ self.agent_command = agent_command
53
+ self.cwd = cwd or str(Path.cwd())
54
+ self.env = env or {}
55
+ self._connection: ClientSideConnection | None = None
56
+ self._session_id: str | None = None
57
+
58
+ async def _ensure_connected(self) -> None:
59
+ """Ensure connection to ACP agent is established."""
60
+ if self._connection is not None:
61
+ return
62
+
63
+ # Spawn agent process
64
+ process = await asyncio.create_subprocess_exec(
65
+ *self.agent_command,
66
+ stdin=asyncio.subprocess.PIPE,
67
+ stdout=asyncio.subprocess.PIPE,
68
+ cwd=self.cwd,
69
+ env={**self.env, **dict(asyncio.get_event_loop().get_environ())},
70
+ )
71
+
72
+ if process.stdin is None or process.stdout is None:
73
+ raise RuntimeError("Agent process does not expose stdio pipes")
74
+
75
+ # Create client implementation
76
+ client_impl = _ACPClientImpl()
77
+ self._connection = connect_to_agent(client_impl, process.stdin, process.stdout)
78
+
79
+ # Initialize connection
80
+ await self._connection.initialize(
81
+ protocol_version=PROTOCOL_VERSION,
82
+ client_capabilities=ClientCapabilities(),
83
+ client_info=Implementation(
84
+ name="codeoptix",
85
+ title="CodeOptiX",
86
+ version="0.1.0",
87
+ ),
88
+ )
89
+
90
+ # Create new session
91
+ session = await self._connection.new_session(mcp_servers=[], cwd=self.cwd)
92
+ self._session_id = session.session_id
93
+
94
+ async def generate_code(
95
+ self,
96
+ prompt: str,
97
+ context: dict[str, Any] | None = None,
98
+ **kwargs: Any,
99
+ ) -> AgentOutput:
100
+ """Generate code using the ACP agent.
101
+
102
+ Args:
103
+ prompt: The prompt for code generation
104
+ context: Additional context
105
+ **kwargs: Additional arguments
106
+
107
+ Returns:
108
+ AgentOutput with generated code
109
+ """
110
+ await self._ensure_connected()
111
+
112
+ if not self._connection or not self._session_id:
113
+ raise RuntimeError("Not connected to ACP agent")
114
+
115
+ # Send prompt to agent
116
+ response = await self._connection.prompt(
117
+ session_id=self._session_id,
118
+ prompt=[text_block(prompt)],
119
+ )
120
+
121
+ # Extract code and messages from response
122
+ code_blocks = []
123
+ messages = []
124
+ code_content = ""
125
+
126
+ # Extract from response messages if available
127
+ if hasattr(response, "messages") and response.messages:
128
+ for message in response.messages:
129
+ # Extract text content
130
+ if hasattr(message, "content"):
131
+ content = message.content
132
+ if isinstance(content, str):
133
+ messages.append(content)
134
+ # Extract code from text
135
+ code_blocks.extend(extract_code_from_text(content))
136
+ elif hasattr(content, "text"):
137
+ text = getattr(content, "text", "")
138
+ if text:
139
+ messages.append(text)
140
+ code_blocks.extend(extract_code_from_text(text))
141
+
142
+ # Extract from response updates if available
143
+ if hasattr(response, "updates") and response.updates:
144
+ code_blocks.extend(extract_all_code(response.updates))
145
+ for update in response.updates:
146
+ if hasattr(update, "content"):
147
+ content = update.content
148
+ if isinstance(content, str):
149
+ messages.append(content)
150
+ elif hasattr(content, "text"):
151
+ messages.append(getattr(content, "text", ""))
152
+
153
+ # Combine all code blocks
154
+ if code_blocks:
155
+ # Prefer code blocks over inline code
156
+ block_codes = [cb["content"] for cb in code_blocks if cb.get("type") == "block"]
157
+ if block_codes:
158
+ code_content = "\n\n".join(block_codes)
159
+ else:
160
+ # Fallback to inline code
161
+ inline_codes = [cb["content"] for cb in code_blocks if cb.get("type") == "inline"]
162
+ if inline_codes:
163
+ code_content = "\n".join(inline_codes)
164
+
165
+ return AgentOutput(
166
+ code=code_content,
167
+ tests="",
168
+ messages=messages,
169
+ metadata={
170
+ "acp_session_id": self._session_id,
171
+ "stop_reason": response.stop_reason if hasattr(response, "stop_reason") else None,
172
+ "code_blocks_count": len(code_blocks),
173
+ },
174
+ )
175
+
176
+ async def close(self) -> None:
177
+ """Close the ACP connection."""
178
+ if self._connection:
179
+ try:
180
+ # Try to gracefully close the connection
181
+ # Note: ACP doesn't have an explicit close method, but we can clean up
182
+ if hasattr(self._connection, "close"):
183
+ await self._connection.close()
184
+ except Exception as e:
185
+ logger.warning(f"Error closing ACP connection: {e}")
186
+ finally:
187
+ self._connection = None
188
+ self._session_id = None
189
+
190
+
191
+ class _ACPClientImpl(Client):
192
+ """Internal client implementation for ACP adapter."""
193
+
194
+ async def request_permission(
195
+ self,
196
+ options: list,
197
+ session_id: str,
198
+ tool_call: Any,
199
+ **kwargs: Any,
200
+ ) -> RequestPermissionResponse:
201
+ """Handle permission requests."""
202
+ # Auto-approve for now (can be made configurable)
203
+ return RequestPermissionResponse(granted=True)
204
+
205
+ async def write_text_file(
206
+ self,
207
+ content: str,
208
+ path: str,
209
+ session_id: str,
210
+ **kwargs: Any,
211
+ ) -> WriteTextFileResponse | None:
212
+ """Handle file write requests."""
213
+ # Allow file writes
214
+ return WriteTextFileResponse()
215
+
216
+ async def read_text_file(
217
+ self,
218
+ path: str,
219
+ session_id: str,
220
+ limit: int | None = None,
221
+ line: int | None = None,
222
+ **kwargs: Any,
223
+ ) -> ReadTextFileResponse:
224
+ """Handle file read requests."""
225
+ try:
226
+ with open(path, encoding="utf-8") as f:
227
+ if line is not None:
228
+ lines = f.readlines()
229
+ if 0 <= line < len(lines):
230
+ content = lines[line]
231
+ else:
232
+ content = ""
233
+ elif limit is not None:
234
+ content = f.read(limit)
235
+ else:
236
+ content = f.read()
237
+ return ReadTextFileResponse(content=content)
238
+ except Exception as e:
239
+ logger.error(f"Error reading file {path}: {e}")
240
+ raise RequestError.internal_error(f"Failed to read file: {e}")
241
+
242
+ async def create_terminal(
243
+ self,
244
+ command: str,
245
+ session_id: str,
246
+ args: list[str] | None = None,
247
+ cwd: str | None = None,
248
+ env: list | None = None,
249
+ output_byte_limit: int | None = None,
250
+ **kwargs: Any,
251
+ ) -> CreateTerminalResponse:
252
+ """Handle terminal creation requests."""
253
+ raise RequestError.method_not_found("terminal/create")
254
+
255
+ async def terminal_output(
256
+ self,
257
+ session_id: str,
258
+ terminal_id: str,
259
+ **kwargs: Any,
260
+ ) -> Any:
261
+ """Handle terminal output requests."""
262
+ raise RequestError.method_not_found("terminal/output")
263
+
264
+ async def release_terminal(
265
+ self,
266
+ session_id: str,
267
+ terminal_id: str,
268
+ **kwargs: Any,
269
+ ) -> Any:
270
+ """Handle terminal release requests."""
271
+ raise RequestError.method_not_found("terminal/release")
272
+
273
+ async def wait_for_terminal_exit(
274
+ self,
275
+ session_id: str,
276
+ terminal_id: str,
277
+ **kwargs: Any,
278
+ ) -> Any:
279
+ """Handle terminal exit wait requests."""
280
+ raise RequestError.method_not_found("terminal/wait_for_exit")
281
+
282
+ async def kill_terminal(
283
+ self,
284
+ session_id: str,
285
+ terminal_id: str,
286
+ **kwargs: Any,
287
+ ) -> Any:
288
+ """Handle terminal kill requests."""
289
+ raise RequestError.method_not_found("terminal/kill")
290
+
291
+ async def session_update(
292
+ self,
293
+ session_id: str,
294
+ update: Any,
295
+ **kwargs: Any,
296
+ ) -> None:
297
+ """Handle session updates from agent."""
298
+ # Log updates for debugging
299
+ if isinstance(update, AgentMessageChunk):
300
+ logger.debug(f"Agent message: {update}")
301
+
302
+ async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
303
+ """Handle extension methods."""
304
+ raise RequestError.method_not_found(method)
305
+
306
+ async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
307
+ """Handle extension notifications."""
308
+ logger.debug(f"Extension notification: {method}")
309
+
310
+ def on_connect(self, conn: Any) -> None:
311
+ """Called when client connects to agent."""
312
+ logger.debug("Connected to ACP agent")
@@ -0,0 +1,125 @@
1
+ """Code extraction from ACP messages and tool calls."""
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+ from acp.schema import (
7
+ AgentMessageChunk,
8
+ FileEditToolCallContent,
9
+ TextContentBlock,
10
+ ToolCallProgress,
11
+ ToolCallStart,
12
+ )
13
+
14
+ # Patterns for extracting code from text
15
+ CODE_BLOCK_PATTERN = re.compile(
16
+ r"```(?:python|py|javascript|js|typescript|ts|java|go|rust|cpp|c\+\+|c|ruby|php|swift|kotlin|scala|r|sql|html|css|xml|yaml|yml|json|bash|sh|shell|zsh|fish)?\n(.*?)```",
17
+ re.DOTALL,
18
+ )
19
+ INLINE_CODE_PATTERN = re.compile(r"`([^`]+)`")
20
+
21
+
22
+ def extract_code_from_text(text: str) -> list[dict[str, str]]:
23
+ """Extract code blocks from text.
24
+
25
+ Args:
26
+ text: Text content to extract code from
27
+
28
+ Returns:
29
+ List of code blocks with language and content
30
+ """
31
+ code_blocks = []
32
+
33
+ # Extract fenced code blocks
34
+ for match in CODE_BLOCK_PATTERN.finditer(text):
35
+ language = match.group(1) or "text"
36
+ code = match.group(2).strip()
37
+ if code:
38
+ code_blocks.append({"language": language, "content": code, "type": "block"})
39
+
40
+ # Extract inline code (less reliable, but useful)
41
+ for match in INLINE_CODE_PATTERN.finditer(text):
42
+ code = match.group(1).strip()
43
+ if len(code) > 10: # Only consider longer inline code
44
+ code_blocks.append({"language": "text", "content": code, "type": "inline"})
45
+
46
+ return code_blocks
47
+
48
+
49
+ def extract_code_from_message(update: Any) -> list[dict[str, str]]:
50
+ """Extract code from an ACP message update.
51
+
52
+ Args:
53
+ update: ACP session update (AgentMessageChunk, ToolCallStart, etc.)
54
+
55
+ Returns:
56
+ List of code blocks extracted from the message
57
+ """
58
+ code_blocks = []
59
+
60
+ if isinstance(update, AgentMessageChunk):
61
+ content = update.content
62
+ if isinstance(content, TextContentBlock):
63
+ # Extract code from text content
64
+ code_blocks.extend(extract_code_from_text(content.text))
65
+ elif hasattr(content, "text"):
66
+ # Fallback for other content types with text
67
+ code_blocks.extend(extract_code_from_text(getattr(content, "text", "")))
68
+
69
+ elif isinstance(update, ToolCallStart):
70
+ tool_call = update.tool_call
71
+ if tool_call.kind == "file_edit":
72
+ content = tool_call.content
73
+ if isinstance(content, FileEditToolCallContent):
74
+ # Extract code from file edits
75
+ if hasattr(content, "old_text") and content.old_text:
76
+ code_blocks.append(
77
+ {
78
+ "language": "text",
79
+ "content": content.old_text,
80
+ "type": "file_edit_old",
81
+ "path": getattr(content, "path", ""),
82
+ }
83
+ )
84
+ if hasattr(content, "new_text") and content.new_text:
85
+ code_blocks.append(
86
+ {
87
+ "language": "text",
88
+ "content": content.new_text,
89
+ "type": "file_edit_new",
90
+ "path": getattr(content, "path", ""),
91
+ }
92
+ )
93
+
94
+ elif isinstance(update, ToolCallProgress):
95
+ tool_call = update.tool_call
96
+ if tool_call.kind == "file_edit":
97
+ content = tool_call.content
98
+ if isinstance(content, FileEditToolCallContent):
99
+ # Extract code from file edit progress
100
+ if hasattr(content, "new_text") and content.new_text:
101
+ code_blocks.append(
102
+ {
103
+ "language": "text",
104
+ "content": content.new_text,
105
+ "type": "file_edit_progress",
106
+ "path": getattr(content, "path", ""),
107
+ }
108
+ )
109
+
110
+ return code_blocks
111
+
112
+
113
+ def extract_all_code(updates: list[Any]) -> list[dict[str, str]]:
114
+ """Extract all code from a list of ACP updates.
115
+
116
+ Args:
117
+ updates: List of ACP session updates
118
+
119
+ Returns:
120
+ List of all code blocks found
121
+ """
122
+ all_code = []
123
+ for update in updates:
124
+ all_code.extend(extract_code_from_message(update))
125
+ return all_code