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,370 @@
|
|
|
1
|
+
"""Gemini CLI adapter for CodeOptix.
|
|
2
|
+
|
|
3
|
+
This adapter interfaces with Google Gemini CLI, which uses the Google Generative AI
|
|
4
|
+
SDK for agent-based code generation and execution. The adapter executes the CLI via
|
|
5
|
+
subprocess in non-interactive mode and parses JSON output.
|
|
6
|
+
|
|
7
|
+
Note: Gemini CLI is a full CLI tool that executes code and uses tools. This adapter
|
|
8
|
+
provides a simplified interface for CodeOptix's evaluation framework.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from codeoptix.adapters.base import AgentAdapter, AgentOutput
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GeminiCLIAdapter(AgentAdapter):
|
|
20
|
+
"""
|
|
21
|
+
Adapter for Google Gemini CLI.
|
|
22
|
+
|
|
23
|
+
Gemini CLI is a coding agent that uses Google's Generative AI SDK. This adapter
|
|
24
|
+
executes Gemini CLI via subprocess in non-interactive mode with JSON output
|
|
25
|
+
to capture structured responses from the agent.
|
|
26
|
+
|
|
27
|
+
The adapter uses `gemini` command in non-interactive mode with JSON output
|
|
28
|
+
to capture the agent's responses, code generation, and tool execution.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: dict[str, Any]):
|
|
32
|
+
"""Initialize Gemini CLI adapter."""
|
|
33
|
+
super().__init__(config)
|
|
34
|
+
|
|
35
|
+
# Get LLM configuration
|
|
36
|
+
llm_config = config.get("llm_config", {})
|
|
37
|
+
self.api_key = (
|
|
38
|
+
llm_config.get("api_key") or os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
|
39
|
+
)
|
|
40
|
+
self.model = llm_config.get("model", "gemini-2.0-flash-exp")
|
|
41
|
+
|
|
42
|
+
# Gemini CLI path (defaults to system PATH)
|
|
43
|
+
self.gemini_path = config.get("gemini_path") or self._find_gemini_path()
|
|
44
|
+
|
|
45
|
+
# Working directory for Gemini execution
|
|
46
|
+
self.working_directory = config.get("working_directory") or os.getcwd()
|
|
47
|
+
|
|
48
|
+
# Output format (json or stream-json)
|
|
49
|
+
self.output_format = config.get("output_format", "json")
|
|
50
|
+
|
|
51
|
+
# Get initial prompt if provided
|
|
52
|
+
self._current_prompt = config.get("prompt") or self._get_default_prompt()
|
|
53
|
+
|
|
54
|
+
def _find_gemini_path(self) -> str:
|
|
55
|
+
"""Find Gemini CLI executable path."""
|
|
56
|
+
# First check if gemini is in PATH
|
|
57
|
+
import shutil
|
|
58
|
+
|
|
59
|
+
gemini_path = shutil.which("gemini")
|
|
60
|
+
if gemini_path:
|
|
61
|
+
return gemini_path
|
|
62
|
+
|
|
63
|
+
# Fallback: try common installation locations
|
|
64
|
+
possible_paths = [
|
|
65
|
+
os.path.expanduser("~/.local/bin/gemini"),
|
|
66
|
+
"/usr/local/bin/gemini",
|
|
67
|
+
"/opt/homebrew/bin/gemini", # macOS Homebrew
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
for path in possible_paths:
|
|
71
|
+
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
72
|
+
return path
|
|
73
|
+
|
|
74
|
+
# If not found, return "gemini" and let subprocess handle the error
|
|
75
|
+
return "gemini"
|
|
76
|
+
|
|
77
|
+
def _get_default_prompt(self) -> str:
|
|
78
|
+
"""Get default Gemini CLI system prompt."""
|
|
79
|
+
return """You are a helpful coding assistant. Write clean, secure, and well-tested code.
|
|
80
|
+
Follow best practices:
|
|
81
|
+
- Write secure code (no hardcoded secrets, validate inputs)
|
|
82
|
+
- Write meaningful tests
|
|
83
|
+
- Follow the user's requirements"""
|
|
84
|
+
|
|
85
|
+
def execute(self, prompt: str, context: dict[str, Any] | None = None) -> AgentOutput:
|
|
86
|
+
"""
|
|
87
|
+
Execute Gemini CLI with a task prompt.
|
|
88
|
+
|
|
89
|
+
Uses `gemini` command in non-interactive mode with JSON output to capture
|
|
90
|
+
the agent's responses, code generation, and tool execution.
|
|
91
|
+
"""
|
|
92
|
+
context = context or {}
|
|
93
|
+
|
|
94
|
+
# Build the full prompt with context
|
|
95
|
+
full_prompt = self._build_prompt(prompt, context)
|
|
96
|
+
|
|
97
|
+
# Execute Gemini CLI
|
|
98
|
+
try:
|
|
99
|
+
result = self._execute_gemini_cli(full_prompt)
|
|
100
|
+
|
|
101
|
+
# Parse JSON output to extract code and tests
|
|
102
|
+
code, tests = self._extract_from_json(result)
|
|
103
|
+
|
|
104
|
+
return AgentOutput(
|
|
105
|
+
code=code,
|
|
106
|
+
tests=tests,
|
|
107
|
+
traces=result.get("traces", []),
|
|
108
|
+
metadata={
|
|
109
|
+
"model": self.model,
|
|
110
|
+
"provider": "google",
|
|
111
|
+
"gemini_version": result.get("gemini_version"),
|
|
112
|
+
"stats": result.get("stats"),
|
|
113
|
+
},
|
|
114
|
+
prompt_used=self._current_prompt,
|
|
115
|
+
)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
return AgentOutput(
|
|
118
|
+
code="",
|
|
119
|
+
tests=None,
|
|
120
|
+
traces=[{"type": "error", "error": str(e)}],
|
|
121
|
+
metadata={"error": str(e)},
|
|
122
|
+
prompt_used=self._current_prompt,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _build_prompt(self, prompt: str, context: dict[str, Any]) -> str:
|
|
126
|
+
"""Build the full prompt including system instructions and context."""
|
|
127
|
+
parts = []
|
|
128
|
+
|
|
129
|
+
# Add system prompt if available
|
|
130
|
+
if self._current_prompt:
|
|
131
|
+
parts.append(f"System instructions: {self._current_prompt}\n")
|
|
132
|
+
|
|
133
|
+
# Add context
|
|
134
|
+
if context:
|
|
135
|
+
if "plan" in context:
|
|
136
|
+
parts.append(f"Plan: {context['plan']}\n")
|
|
137
|
+
if "requirements" in context:
|
|
138
|
+
reqs = context["requirements"]
|
|
139
|
+
if isinstance(reqs, list):
|
|
140
|
+
reqs = "\n".join(f"- {r}" for r in reqs)
|
|
141
|
+
parts.append(f"Requirements:\n{reqs}\n")
|
|
142
|
+
if "files" in context:
|
|
143
|
+
parts.append("Files:")
|
|
144
|
+
for file_path, content in context["files"].items():
|
|
145
|
+
parts.append(f"\n{file_path}:\n{content}")
|
|
146
|
+
|
|
147
|
+
# Add the main task
|
|
148
|
+
parts.append(f"\nTask: {prompt}")
|
|
149
|
+
|
|
150
|
+
return "\n".join(parts)
|
|
151
|
+
|
|
152
|
+
def _execute_gemini_cli(self, prompt: str) -> dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Execute Gemini CLI via subprocess and parse JSON output.
|
|
155
|
+
|
|
156
|
+
Uses `gemini` command with `--json` or `--stream-json` flag to get
|
|
157
|
+
structured output in non-interactive mode.
|
|
158
|
+
"""
|
|
159
|
+
# Build command arguments
|
|
160
|
+
cmd = [self.gemini_path]
|
|
161
|
+
|
|
162
|
+
# Add output format (--output-format json or --output-format stream-json)
|
|
163
|
+
output_format = "stream-json" if self.output_format == "stream-json" else "json"
|
|
164
|
+
cmd.extend(["--output-format", output_format])
|
|
165
|
+
|
|
166
|
+
# Add model if specified
|
|
167
|
+
if self.model:
|
|
168
|
+
cmd.extend(["--model", self.model])
|
|
169
|
+
|
|
170
|
+
# Set up environment
|
|
171
|
+
env = os.environ.copy()
|
|
172
|
+
if self.api_key:
|
|
173
|
+
env["GOOGLE_API_KEY"] = self.api_key
|
|
174
|
+
env["GEMINI_API_KEY"] = self.api_key
|
|
175
|
+
|
|
176
|
+
# Execute and capture output
|
|
177
|
+
try:
|
|
178
|
+
process = subprocess.Popen(
|
|
179
|
+
cmd,
|
|
180
|
+
stdin=subprocess.PIPE,
|
|
181
|
+
stdout=subprocess.PIPE,
|
|
182
|
+
stderr=subprocess.PIPE,
|
|
183
|
+
text=True,
|
|
184
|
+
env=env,
|
|
185
|
+
cwd=self.working_directory,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
stdout, stderr = process.communicate(input=prompt, timeout=300)
|
|
189
|
+
|
|
190
|
+
if process.returncode != 0:
|
|
191
|
+
raise RuntimeError(f"Gemini CLI exited with code {process.returncode}: {stderr}")
|
|
192
|
+
|
|
193
|
+
# Parse JSON output
|
|
194
|
+
return self._parse_json_output(stdout, stderr)
|
|
195
|
+
|
|
196
|
+
except subprocess.TimeoutExpired:
|
|
197
|
+
process.kill()
|
|
198
|
+
raise RuntimeError("Gemini CLI execution timed out after 300 seconds")
|
|
199
|
+
except FileNotFoundError:
|
|
200
|
+
raise RuntimeError(
|
|
201
|
+
f"Gemini CLI not found at '{self.gemini_path}'. "
|
|
202
|
+
"Please install Gemini CLI: npm install -g @google/gemini-cli"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _parse_json_output(self, stdout: str, stderr: str) -> dict[str, Any]:
|
|
206
|
+
"""
|
|
207
|
+
Parse JSON output from Gemini CLI.
|
|
208
|
+
|
|
209
|
+
Gemini CLI outputs JSON in the following formats:
|
|
210
|
+
- JSON: Single JSON object with session_id, content, stats
|
|
211
|
+
- Stream JSON: JSON Lines (JSONL) with events (INIT, MESSAGE, TOOL_USE, TOOL_RESULT, RESULT)
|
|
212
|
+
"""
|
|
213
|
+
traces = []
|
|
214
|
+
content = ""
|
|
215
|
+
stats = None
|
|
216
|
+
tool_calls = []
|
|
217
|
+
|
|
218
|
+
# Try to parse as single JSON object first
|
|
219
|
+
try:
|
|
220
|
+
data = json.loads(stdout.strip())
|
|
221
|
+
|
|
222
|
+
# Standard JSON format
|
|
223
|
+
if "content" in data:
|
|
224
|
+
content = data.get("content", "")
|
|
225
|
+
stats = data.get("stats")
|
|
226
|
+
traces.append({"type": "json_response", "data": data})
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"traces": traces,
|
|
230
|
+
"content": content,
|
|
231
|
+
"stats": stats,
|
|
232
|
+
"tool_calls": tool_calls,
|
|
233
|
+
"stderr": stderr,
|
|
234
|
+
}
|
|
235
|
+
except json.JSONDecodeError:
|
|
236
|
+
# Try parsing as JSONL (stream-json format)
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
# Parse as JSONL (stream-json format)
|
|
240
|
+
for line in stdout.strip().split("\n"):
|
|
241
|
+
if not line.strip():
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
event = json.loads(line)
|
|
246
|
+
event_type = event.get("type", "")
|
|
247
|
+
|
|
248
|
+
traces.append(event)
|
|
249
|
+
|
|
250
|
+
if event_type == "MESSAGE":
|
|
251
|
+
# Accumulate message content
|
|
252
|
+
if event.get("role") == "assistant":
|
|
253
|
+
content += event.get("content", "")
|
|
254
|
+
|
|
255
|
+
elif event_type == "TOOL_USE":
|
|
256
|
+
tool_calls.append(
|
|
257
|
+
{
|
|
258
|
+
"tool_name": event.get("tool_name"),
|
|
259
|
+
"tool_id": event.get("tool_id"),
|
|
260
|
+
"parameters": event.get("parameters"),
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
elif event_type == "TOOL_RESULT":
|
|
265
|
+
# Tool execution result
|
|
266
|
+
tool_calls.append(
|
|
267
|
+
{
|
|
268
|
+
"tool_id": event.get("tool_id"),
|
|
269
|
+
"status": event.get("status"),
|
|
270
|
+
"output": event.get("output"),
|
|
271
|
+
"error": event.get("error"),
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
elif event_type == "RESULT":
|
|
276
|
+
stats = event.get("stats")
|
|
277
|
+
|
|
278
|
+
except json.JSONDecodeError:
|
|
279
|
+
# Skip invalid JSON lines
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"traces": traces,
|
|
284
|
+
"content": content,
|
|
285
|
+
"stats": stats,
|
|
286
|
+
"tool_calls": tool_calls,
|
|
287
|
+
"stderr": stderr,
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
def _extract_from_json(self, result: dict[str, Any]) -> tuple[str, str | None]:
|
|
291
|
+
"""
|
|
292
|
+
Extract code and tests from Gemini JSON output.
|
|
293
|
+
|
|
294
|
+
Looks for:
|
|
295
|
+
- Code in markdown code blocks in message content
|
|
296
|
+
- Tool calls that create/modify files (file_write, file_edit tools)
|
|
297
|
+
- Test files in tool results
|
|
298
|
+
"""
|
|
299
|
+
content = result.get("content", "")
|
|
300
|
+
tool_calls = result.get("tool_calls", [])
|
|
301
|
+
code = ""
|
|
302
|
+
tests = None
|
|
303
|
+
|
|
304
|
+
# Extract code from message content (markdown code blocks)
|
|
305
|
+
import re
|
|
306
|
+
|
|
307
|
+
code_blocks = re.findall(
|
|
308
|
+
r"```(?:python|javascript|typescript)?\n(.*?)```", content, re.DOTALL
|
|
309
|
+
)
|
|
310
|
+
if code_blocks:
|
|
311
|
+
code = code_blocks[0].strip()
|
|
312
|
+
|
|
313
|
+
# Look for file operations in tool calls
|
|
314
|
+
code_files = []
|
|
315
|
+
test_files = []
|
|
316
|
+
|
|
317
|
+
for tool_call in tool_calls:
|
|
318
|
+
tool_name = tool_call.get("tool_name", "")
|
|
319
|
+
parameters = tool_call.get("parameters", {})
|
|
320
|
+
output = tool_call.get("output", "")
|
|
321
|
+
|
|
322
|
+
# Check for file write/edit operations
|
|
323
|
+
if tool_name in ["file_write", "file_edit", "write_file"]:
|
|
324
|
+
file_path = parameters.get("path") or parameters.get("file_path", "")
|
|
325
|
+
file_content = parameters.get("content") or parameters.get("file_content", "")
|
|
326
|
+
|
|
327
|
+
if file_path.endswith((".py", ".js", ".ts")):
|
|
328
|
+
if "test" in file_path.lower() or "test_" in file_path:
|
|
329
|
+
test_files.append((file_path, file_content))
|
|
330
|
+
else:
|
|
331
|
+
code_files.append((file_path, file_content))
|
|
332
|
+
|
|
333
|
+
# Check tool results for file content
|
|
334
|
+
if output and isinstance(output, str):
|
|
335
|
+
# Try to extract file paths and content from output
|
|
336
|
+
file_matches = re.findall(
|
|
337
|
+
r"File:\s*([^\n]+)\n```[^\n]*\n(.*?)```", output, re.DOTALL
|
|
338
|
+
)
|
|
339
|
+
for file_path, file_content in file_matches:
|
|
340
|
+
if file_path.endswith((".py", ".js", ".ts")):
|
|
341
|
+
if "test" in file_path.lower():
|
|
342
|
+
test_files.append((file_path, file_content))
|
|
343
|
+
else:
|
|
344
|
+
code_files.append((file_path, file_content))
|
|
345
|
+
|
|
346
|
+
# Use file operations if available, otherwise use extracted code
|
|
347
|
+
if code_files:
|
|
348
|
+
# Combine all code files
|
|
349
|
+
code = "\n\n".join(f"# {path}\n{content}" for path, content in code_files)
|
|
350
|
+
elif not code:
|
|
351
|
+
# Fallback to message content
|
|
352
|
+
code = content
|
|
353
|
+
|
|
354
|
+
# Extract tests
|
|
355
|
+
if test_files:
|
|
356
|
+
tests = "\n\n".join(f"# {path}\n{content}" for path, content in test_files)
|
|
357
|
+
|
|
358
|
+
return code, tests
|
|
359
|
+
|
|
360
|
+
def get_prompt(self) -> str:
|
|
361
|
+
"""Get current Gemini CLI prompt."""
|
|
362
|
+
return self._current_prompt or ""
|
|
363
|
+
|
|
364
|
+
def update_prompt(self, new_prompt: str) -> None:
|
|
365
|
+
"""Update Gemini CLI prompt."""
|
|
366
|
+
self._current_prompt = new_prompt
|
|
367
|
+
|
|
368
|
+
def get_adapter_type(self) -> str:
|
|
369
|
+
"""Get adapter type identifier."""
|
|
370
|
+
return "gemini-cli"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Artifact management for CodeOptix."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArtifactManager:
|
|
13
|
+
"""Manages storage and retrieval of CodeOptix artifacts."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, artifacts_dir: str | Path | None = None):
|
|
16
|
+
"""
|
|
17
|
+
Initialize artifact manager.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
artifacts_dir: Directory for storing artifacts (default: .codeoptix/artifacts)
|
|
21
|
+
"""
|
|
22
|
+
if artifacts_dir is None:
|
|
23
|
+
artifacts_dir = Path(".codeoptix") / "artifacts"
|
|
24
|
+
|
|
25
|
+
self.artifacts_dir = Path(artifacts_dir)
|
|
26
|
+
self.artifacts_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
def save_results(self, results: dict[str, Any], run_id: str | None = None) -> Path:
|
|
29
|
+
"""
|
|
30
|
+
Save evaluation results to JSON file.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
results: Evaluation results dictionary
|
|
34
|
+
run_id: Optional run ID (generated if not provided)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Path to saved results file
|
|
38
|
+
"""
|
|
39
|
+
if run_id is None:
|
|
40
|
+
run_id = self._generate_run_id()
|
|
41
|
+
|
|
42
|
+
# Add metadata
|
|
43
|
+
results_with_metadata = {
|
|
44
|
+
"run_id": run_id,
|
|
45
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
46
|
+
"version": "0.1.0",
|
|
47
|
+
**results,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Save to file
|
|
51
|
+
results_file = self.artifacts_dir / f"results_{run_id}.json"
|
|
52
|
+
with open(results_file, "w") as f:
|
|
53
|
+
json.dump(results_with_metadata, f, indent=2, default=str)
|
|
54
|
+
|
|
55
|
+
return results_file
|
|
56
|
+
|
|
57
|
+
def load_results(self, run_id: str) -> dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Load evaluation results by run ID.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
run_id: Run ID to load
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Results dictionary
|
|
66
|
+
"""
|
|
67
|
+
results_file = self.artifacts_dir / f"results_{run_id}.json"
|
|
68
|
+
|
|
69
|
+
if not results_file.exists():
|
|
70
|
+
raise FileNotFoundError(f"Results file not found: {results_file}")
|
|
71
|
+
|
|
72
|
+
with open(results_file) as f:
|
|
73
|
+
return json.load(f)
|
|
74
|
+
|
|
75
|
+
def save_reflection(self, reflection_content: str, run_id: str | None = None) -> Path:
|
|
76
|
+
"""
|
|
77
|
+
Save reflection markdown file.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
reflection_content: Reflection markdown content
|
|
81
|
+
run_id: Optional run ID (generated if not provided)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Path to saved reflection file
|
|
85
|
+
"""
|
|
86
|
+
if run_id is None:
|
|
87
|
+
run_id = self._generate_run_id()
|
|
88
|
+
|
|
89
|
+
reflection_file = self.artifacts_dir / f"reflection_{run_id}.md"
|
|
90
|
+
with open(reflection_file, "w") as f:
|
|
91
|
+
f.write(reflection_content)
|
|
92
|
+
|
|
93
|
+
return reflection_file
|
|
94
|
+
|
|
95
|
+
def load_reflection(self, run_id: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Load reflection markdown by run ID.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
run_id: Run ID to load
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Reflection markdown content
|
|
104
|
+
"""
|
|
105
|
+
reflection_file = self.artifacts_dir / f"reflection_{run_id}.md"
|
|
106
|
+
|
|
107
|
+
if not reflection_file.exists():
|
|
108
|
+
raise FileNotFoundError(f"Reflection file not found: {reflection_file}")
|
|
109
|
+
|
|
110
|
+
with open(reflection_file) as f:
|
|
111
|
+
return f.read()
|
|
112
|
+
|
|
113
|
+
def save_evolved_prompts(
|
|
114
|
+
self, evolved_prompts: dict[str, Any], run_id: str | None = None
|
|
115
|
+
) -> Path:
|
|
116
|
+
"""
|
|
117
|
+
Save evolved prompts to YAML file.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
evolved_prompts: Evolved prompts dictionary
|
|
121
|
+
run_id: Optional run ID (generated if not provided)
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Path to saved prompts file
|
|
125
|
+
"""
|
|
126
|
+
if run_id is None:
|
|
127
|
+
run_id = self._generate_run_id()
|
|
128
|
+
|
|
129
|
+
# Add metadata
|
|
130
|
+
prompts_with_metadata = {
|
|
131
|
+
"version": "0.1.0",
|
|
132
|
+
"run_id": run_id,
|
|
133
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
134
|
+
**evolved_prompts,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
prompts_file = self.artifacts_dir / f"evolved_prompts_{run_id}.yaml"
|
|
138
|
+
with open(prompts_file, "w") as f:
|
|
139
|
+
yaml.dump(prompts_with_metadata, f, default_flow_style=False, sort_keys=False)
|
|
140
|
+
|
|
141
|
+
return prompts_file
|
|
142
|
+
|
|
143
|
+
def load_evolved_prompts(self, run_id: str) -> dict[str, Any]:
|
|
144
|
+
"""
|
|
145
|
+
Load evolved prompts by run ID.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
run_id: Run ID to load
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Evolved prompts dictionary
|
|
152
|
+
"""
|
|
153
|
+
prompts_file = self.artifacts_dir / f"evolved_prompts_{run_id}.yaml"
|
|
154
|
+
|
|
155
|
+
if not prompts_file.exists():
|
|
156
|
+
raise FileNotFoundError(f"Evolved prompts file not found: {prompts_file}")
|
|
157
|
+
|
|
158
|
+
with open(prompts_file) as f:
|
|
159
|
+
return yaml.safe_load(f)
|
|
160
|
+
|
|
161
|
+
def list_runs(self) -> list[dict[str, Any]]:
|
|
162
|
+
"""
|
|
163
|
+
List all evaluation runs.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of run metadata dictionaries
|
|
167
|
+
"""
|
|
168
|
+
runs = []
|
|
169
|
+
|
|
170
|
+
# Find all results files
|
|
171
|
+
for results_file in self.artifacts_dir.glob("results_*.json"):
|
|
172
|
+
try:
|
|
173
|
+
run_id = results_file.stem.replace("results_", "")
|
|
174
|
+
with open(results_file) as f:
|
|
175
|
+
data = json.load(f)
|
|
176
|
+
runs.append(
|
|
177
|
+
{
|
|
178
|
+
"run_id": run_id,
|
|
179
|
+
"timestamp": data.get("timestamp"),
|
|
180
|
+
"overall_score": data.get("overall_score"),
|
|
181
|
+
"behaviors": list(data.get("behaviors", {}).keys()),
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
except Exception:
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# Sort by timestamp (newest first)
|
|
188
|
+
runs.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
|
189
|
+
return runs
|
|
190
|
+
|
|
191
|
+
def _generate_run_id(self) -> str:
|
|
192
|
+
"""Generate a unique run ID."""
|
|
193
|
+
return str(uuid4())[:8]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Behavior specifications for CodeOptix."""
|
|
2
|
+
|
|
3
|
+
from codeoptix.behaviors.base import BehaviorResult, BehaviorSpec, Severity
|
|
4
|
+
from codeoptix.behaviors.insecure_code import InsecureCodeBehavior
|
|
5
|
+
from codeoptix.behaviors.plan_drift import PlanDriftBehavior
|
|
6
|
+
from codeoptix.behaviors.vacuous_tests import VacuousTestsBehavior
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BehaviorResult",
|
|
10
|
+
"BehaviorSpec",
|
|
11
|
+
"InsecureCodeBehavior",
|
|
12
|
+
"PlanDriftBehavior",
|
|
13
|
+
"Severity",
|
|
14
|
+
"VacuousTestsBehavior",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# Registry of available behaviors
|
|
18
|
+
BEHAVIOR_REGISTRY = {
|
|
19
|
+
"insecure-code": InsecureCodeBehavior,
|
|
20
|
+
"vacuous-tests": VacuousTestsBehavior,
|
|
21
|
+
"plan-drift": PlanDriftBehavior,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_behavior(name: str, config: dict | None = None) -> BehaviorSpec:
|
|
26
|
+
"""
|
|
27
|
+
Factory function to create a behavior spec.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
name: Behavior name (e.g., "insecure-code")
|
|
31
|
+
config: Optional configuration dictionary
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
BehaviorSpec instance
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If behavior name is not found
|
|
38
|
+
"""
|
|
39
|
+
if name not in BEHAVIOR_REGISTRY:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Unknown behavior: {name}. Available behaviors: {', '.join(BEHAVIOR_REGISTRY.keys())}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
behavior_class = BEHAVIOR_REGISTRY[name]
|
|
45
|
+
return behavior_class(config=config or {})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Base behavior specification interface for CodeOptix."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Severity(str, Enum):
|
|
10
|
+
"""Severity levels for behavior violations."""
|
|
11
|
+
|
|
12
|
+
LOW = "low"
|
|
13
|
+
MEDIUM = "medium"
|
|
14
|
+
HIGH = "high"
|
|
15
|
+
CRITICAL = "critical"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class BehaviorResult:
|
|
20
|
+
"""Result of behavior evaluation."""
|
|
21
|
+
|
|
22
|
+
behavior_name: str
|
|
23
|
+
passed: bool
|
|
24
|
+
score: float # 0.0 to 1.0 (1.0 = perfect, 0.0 = failed)
|
|
25
|
+
evidence: list[str] = field(default_factory=list)
|
|
26
|
+
severity: Severity = Severity.MEDIUM
|
|
27
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def __post_init__(self):
|
|
30
|
+
"""Validate score range."""
|
|
31
|
+
if not 0.0 <= self.score <= 1.0:
|
|
32
|
+
raise ValueError(f"Score must be between 0.0 and 1.0, got {self.score}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BehaviorSpec(ABC):
|
|
36
|
+
"""Base interface for behavior specifications."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
39
|
+
"""
|
|
40
|
+
Initialize behavior spec with configuration.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
config: Behavior-specific configuration dictionary
|
|
44
|
+
"""
|
|
45
|
+
self.config = config or {}
|
|
46
|
+
self.name = self.get_name()
|
|
47
|
+
self.severity = Severity(self.config.get("severity", "medium"))
|
|
48
|
+
self.enabled = self.config.get("enabled", True)
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get_name(self) -> str:
|
|
52
|
+
"""Get the name identifier for this behavior."""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def get_description(self) -> str:
|
|
56
|
+
"""Get human-readable description of behavior."""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def evaluate(
|
|
60
|
+
self,
|
|
61
|
+
agent_output: Any, # AgentOutput from adapters
|
|
62
|
+
context: dict[str, Any] | None = None,
|
|
63
|
+
) -> BehaviorResult:
|
|
64
|
+
"""
|
|
65
|
+
Evaluate agent output against behavior spec.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
agent_output: Output from agent adapter (AgentOutput)
|
|
69
|
+
context: Optional context (files, workspace, planning artifacts, etc.)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
BehaviorResult with evaluation results
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def is_enabled(self) -> bool:
|
|
76
|
+
"""Check if this behavior spec is enabled."""
|
|
77
|
+
return self.enabled
|
|
78
|
+
|
|
79
|
+
def get_severity(self) -> Severity:
|
|
80
|
+
"""Get the severity level for this behavior."""
|
|
81
|
+
return self.severity
|