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.
- 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 +221 -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 +1468 -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 +334 -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.3.dist-info/METADATA +295 -0
- codeoptix-0.1.3.dist-info/RECORD +92 -0
- codeoptix-0.1.3.dist-info/WHEEL +5 -0
- codeoptix-0.1.3.dist-info/entry_points.txt +2 -0
- codeoptix-0.1.3.dist-info/licenses/LICENSE +203 -0
- 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
|