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,186 @@
|
|
|
1
|
+
"""Linter runner that orchestrates multiple linters."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from codeoptix.linters.bandit_linter import BanditLinter
|
|
7
|
+
from codeoptix.linters.base import BaseLinter
|
|
8
|
+
from codeoptix.linters.coverage_linter import CoverageLinter
|
|
9
|
+
from codeoptix.linters.flake8_linter import Flake8Linter
|
|
10
|
+
from codeoptix.linters.html_accessibility_linter import HTMLAccessibilityLinter
|
|
11
|
+
from codeoptix.linters.language_detector import LanguageDetector
|
|
12
|
+
from codeoptix.linters.mypy_linter import MypyLinter
|
|
13
|
+
from codeoptix.linters.pip_audit_linter import PipAuditLinter
|
|
14
|
+
from codeoptix.linters.pylint_linter import PylintLinter
|
|
15
|
+
from codeoptix.linters.ruff_linter import RuffLinter
|
|
16
|
+
from codeoptix.linters.safety_linter import SafetyLinter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LinterRunner:
|
|
20
|
+
"""Runs multiple linters and aggregates results."""
|
|
21
|
+
|
|
22
|
+
# Registry of available linters (zero new dependencies)
|
|
23
|
+
LINTER_REGISTRY = {
|
|
24
|
+
# Code Quality (fastest first)
|
|
25
|
+
"ruff": RuffLinter, # Fastest - should be first
|
|
26
|
+
"pylint": PylintLinter,
|
|
27
|
+
"flake8": Flake8Linter,
|
|
28
|
+
"mypy": MypyLinter, # Type checking
|
|
29
|
+
# Security
|
|
30
|
+
"bandit": BanditLinter,
|
|
31
|
+
"safety": SafetyLinter, # Dependency vulnerabilities
|
|
32
|
+
"pip-audit": PipAuditLinter, # Package audit
|
|
33
|
+
# Testing
|
|
34
|
+
"coverage": CoverageLinter, # Test coverage
|
|
35
|
+
# Accessibility (custom, no dependency)
|
|
36
|
+
"html-accessibility": HTMLAccessibilityLinter, # Custom HTML analyzer
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
40
|
+
"""Initialize linter runner."""
|
|
41
|
+
self.config = config or {}
|
|
42
|
+
self.linters: dict[str, BaseLinter] = {}
|
|
43
|
+
|
|
44
|
+
# Initialize enabled linters
|
|
45
|
+
# Default: Use all available linters (zero new dependencies)
|
|
46
|
+
enabled_linters = self.config.get("linters", list(self.LINTER_REGISTRY.keys()))
|
|
47
|
+
for linter_name in enabled_linters:
|
|
48
|
+
if linter_name in self.LINTER_REGISTRY:
|
|
49
|
+
linter_config = self.config.get("linter_config", {}).get(linter_name, {})
|
|
50
|
+
try:
|
|
51
|
+
self.linters[linter_name] = self.LINTER_REGISTRY[linter_name](linter_config)
|
|
52
|
+
except Exception:
|
|
53
|
+
# Skip linters that fail to initialize
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def run_linters(
|
|
57
|
+
self,
|
|
58
|
+
path: str,
|
|
59
|
+
linter_names: list[str] | None = None,
|
|
60
|
+
files: list[str] | None = None,
|
|
61
|
+
auto_detect: bool = True,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""
|
|
64
|
+
Run specified linters on code.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
path: Path to code (file or directory)
|
|
68
|
+
linter_names: List of linter names to run (None = auto-detect or all enabled)
|
|
69
|
+
files: Optional list of specific files to check
|
|
70
|
+
auto_detect: Auto-detect language and select appropriate linters
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with aggregated results
|
|
74
|
+
"""
|
|
75
|
+
start_time = time.time()
|
|
76
|
+
|
|
77
|
+
# Auto-detect language and linters if requested
|
|
78
|
+
if linter_names is None and auto_detect:
|
|
79
|
+
if files:
|
|
80
|
+
# Detect from file list
|
|
81
|
+
detected_linters = LanguageDetector.get_linters_for_files(files)
|
|
82
|
+
# Find existing configs
|
|
83
|
+
config_files = LanguageDetector.find_config_files(path)
|
|
84
|
+
# Prioritize linters with existing configs
|
|
85
|
+
linter_names = []
|
|
86
|
+
for linter in detected_linters:
|
|
87
|
+
if linter in config_files or linter in self.LINTER_REGISTRY:
|
|
88
|
+
linter_names.append(linter)
|
|
89
|
+
# If no detected linters, use all available
|
|
90
|
+
if not linter_names:
|
|
91
|
+
linter_names = list(self.linters.keys())
|
|
92
|
+
else:
|
|
93
|
+
# Use all enabled linters
|
|
94
|
+
linter_names = list(self.linters.keys())
|
|
95
|
+
elif linter_names is None:
|
|
96
|
+
linter_names = list(self.linters.keys())
|
|
97
|
+
|
|
98
|
+
# Filter to available linters
|
|
99
|
+
available_linters = {
|
|
100
|
+
name: linter
|
|
101
|
+
for name, linter in self.linters.items()
|
|
102
|
+
if name in linter_names and linter.is_available()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if not available_linters:
|
|
106
|
+
return {
|
|
107
|
+
"success": False,
|
|
108
|
+
"errors": [
|
|
109
|
+
"No linters available. Install linters: pip install bandit pylint flake8"
|
|
110
|
+
],
|
|
111
|
+
"results": {},
|
|
112
|
+
"summary": {
|
|
113
|
+
"total_issues": 0,
|
|
114
|
+
"critical": 0,
|
|
115
|
+
"high": 0,
|
|
116
|
+
"medium": 0,
|
|
117
|
+
"low": 0,
|
|
118
|
+
},
|
|
119
|
+
"execution_time": time.time() - start_time,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Run each linter
|
|
123
|
+
results = {}
|
|
124
|
+
all_issues = []
|
|
125
|
+
all_errors = []
|
|
126
|
+
|
|
127
|
+
for linter_name, linter in available_linters.items():
|
|
128
|
+
try:
|
|
129
|
+
result = linter.run(path, files=files)
|
|
130
|
+
results[linter_name] = result.to_dict()
|
|
131
|
+
all_issues.extend(result.issues)
|
|
132
|
+
all_errors.extend(result.errors)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
all_errors.append(f"{linter_name} error: {e!s}")
|
|
135
|
+
results[linter_name] = {
|
|
136
|
+
"success": False,
|
|
137
|
+
"issues": [],
|
|
138
|
+
"errors": [str(e)],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Aggregate summary
|
|
142
|
+
summary = self._aggregate_summary(all_issues)
|
|
143
|
+
|
|
144
|
+
execution_time = time.time() - start_time
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"success": len(all_issues) == 0 and len(all_errors) == 0,
|
|
148
|
+
"results": results,
|
|
149
|
+
"summary": summary,
|
|
150
|
+
"issues": [issue.to_dict() for issue in all_issues],
|
|
151
|
+
"errors": all_errors,
|
|
152
|
+
"execution_time": execution_time,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
def _aggregate_summary(self, issues: list) -> dict[str, int]:
|
|
156
|
+
"""Aggregate issue summary by severity."""
|
|
157
|
+
summary = {
|
|
158
|
+
"total_issues": len(issues),
|
|
159
|
+
"critical": 0,
|
|
160
|
+
"high": 0,
|
|
161
|
+
"medium": 0,
|
|
162
|
+
"low": 0,
|
|
163
|
+
"info": 0,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for issue in issues:
|
|
167
|
+
severity = (
|
|
168
|
+
issue.severity if hasattr(issue, "severity") else issue.get("severity", "low")
|
|
169
|
+
)
|
|
170
|
+
if isinstance(severity, str):
|
|
171
|
+
severity_key = severity.lower()
|
|
172
|
+
else:
|
|
173
|
+
severity_key = severity.value.lower()
|
|
174
|
+
|
|
175
|
+
if severity_key in summary:
|
|
176
|
+
summary[severity_key] += 1
|
|
177
|
+
|
|
178
|
+
return summary
|
|
179
|
+
|
|
180
|
+
def get_available_linters(self) -> list[str]:
|
|
181
|
+
"""Get list of available linters."""
|
|
182
|
+
return [name for name, linter in self.linters.items() if linter.is_available()]
|
|
183
|
+
|
|
184
|
+
def get_all_linters(self) -> list[str]:
|
|
185
|
+
"""Get list of all registered linters."""
|
|
186
|
+
return list(self.LINTER_REGISTRY.keys())
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Safety dependency vulnerability linter integration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from codeoptix.linters.base import BaseLinter, LinterIssue, LinterResult, Severity
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SafetyLinter(BaseLinter):
|
|
11
|
+
"""Safety dependency vulnerability scanner."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: dict | None = None):
|
|
14
|
+
"""Initialize Safety linter."""
|
|
15
|
+
super().__init__(config)
|
|
16
|
+
self.name = "safety"
|
|
17
|
+
|
|
18
|
+
def is_available(self) -> bool:
|
|
19
|
+
"""Check if safety is available."""
|
|
20
|
+
try:
|
|
21
|
+
subprocess.run(
|
|
22
|
+
["safety", "--version"],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
timeout=5,
|
|
25
|
+
check=False,
|
|
26
|
+
)
|
|
27
|
+
return True
|
|
28
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
def run(self, path: str, files: list[str] | None = None) -> LinterResult:
|
|
32
|
+
"""Run safety on dependencies."""
|
|
33
|
+
import time
|
|
34
|
+
|
|
35
|
+
start_time = time.time()
|
|
36
|
+
|
|
37
|
+
if not self.is_available():
|
|
38
|
+
return LinterResult(
|
|
39
|
+
linter=self.name,
|
|
40
|
+
success=False,
|
|
41
|
+
issues=[],
|
|
42
|
+
errors=["Safety not found in PATH. Install with: pip install safety"],
|
|
43
|
+
execution_time=0.0,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Safety checks requirements files or installed packages
|
|
47
|
+
# Find requirements files
|
|
48
|
+
path_obj = Path(path)
|
|
49
|
+
requirements_files = []
|
|
50
|
+
|
|
51
|
+
if path_obj.is_file() and path_obj.name in [
|
|
52
|
+
"requirements.txt",
|
|
53
|
+
"requirements-dev.txt",
|
|
54
|
+
"pyproject.toml",
|
|
55
|
+
]:
|
|
56
|
+
requirements_files = [str(path_obj)]
|
|
57
|
+
elif path_obj.is_dir():
|
|
58
|
+
# Look for requirements files
|
|
59
|
+
for req_file in ["requirements.txt", "requirements-dev.txt", "pyproject.toml"]:
|
|
60
|
+
req_path = path_obj / req_file
|
|
61
|
+
if req_path.exists():
|
|
62
|
+
requirements_files.append(str(req_path))
|
|
63
|
+
|
|
64
|
+
if not requirements_files:
|
|
65
|
+
# Safety can check installed packages without requirements file
|
|
66
|
+
requirements_files = []
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Build command
|
|
70
|
+
cmd = ["safety", "check", "--json"]
|
|
71
|
+
|
|
72
|
+
# Add requirements file if found
|
|
73
|
+
if requirements_files:
|
|
74
|
+
cmd.extend(["--file", requirements_files[0]])
|
|
75
|
+
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
cmd,
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
timeout=60,
|
|
81
|
+
check=False,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
execution_time = time.time() - start_time
|
|
85
|
+
return self.parse_output(
|
|
86
|
+
result.stdout,
|
|
87
|
+
result.stderr,
|
|
88
|
+
result.returncode,
|
|
89
|
+
execution_time,
|
|
90
|
+
)
|
|
91
|
+
except subprocess.TimeoutExpired:
|
|
92
|
+
return LinterResult(
|
|
93
|
+
linter=self.name,
|
|
94
|
+
success=False,
|
|
95
|
+
issues=[],
|
|
96
|
+
errors=["Safety execution timed out"],
|
|
97
|
+
execution_time=60.0,
|
|
98
|
+
)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return LinterResult(
|
|
101
|
+
linter=self.name,
|
|
102
|
+
success=False,
|
|
103
|
+
issues=[],
|
|
104
|
+
errors=[f"Safety error: {e!s}"],
|
|
105
|
+
execution_time=time.time() - start_time,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def parse_output(
|
|
109
|
+
self,
|
|
110
|
+
output: str,
|
|
111
|
+
stderr: str,
|
|
112
|
+
returncode: int,
|
|
113
|
+
execution_time: float,
|
|
114
|
+
) -> LinterResult:
|
|
115
|
+
"""Parse safety JSON output."""
|
|
116
|
+
issues = []
|
|
117
|
+
errors = []
|
|
118
|
+
|
|
119
|
+
if stderr:
|
|
120
|
+
errors.append(stderr)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
data = json.loads(output) if output.strip() else []
|
|
124
|
+
|
|
125
|
+
for item in data:
|
|
126
|
+
package = item.get("package", "unknown")
|
|
127
|
+
vulnerability = item.get("vulnerability", "")
|
|
128
|
+
installed_version = item.get("installed_version", "")
|
|
129
|
+
affected_versions = item.get("affected_versions", "")
|
|
130
|
+
cve = item.get("CVE", "")
|
|
131
|
+
|
|
132
|
+
# Safety vulnerabilities are always high/critical
|
|
133
|
+
severity = Severity.HIGH
|
|
134
|
+
if "critical" in vulnerability.lower() or "critical" in str(item).lower():
|
|
135
|
+
severity = Severity.CRITICAL
|
|
136
|
+
|
|
137
|
+
message = f"{package} {installed_version}: {vulnerability}"
|
|
138
|
+
if cve:
|
|
139
|
+
message += f" (CVE: {cve})"
|
|
140
|
+
if affected_versions:
|
|
141
|
+
message += f" [Affected: {affected_versions}]"
|
|
142
|
+
|
|
143
|
+
issue = LinterIssue(
|
|
144
|
+
linter=self.name,
|
|
145
|
+
severity=severity,
|
|
146
|
+
message=message,
|
|
147
|
+
file="requirements.txt", # Safety checks dependencies
|
|
148
|
+
rule_id=cve or f"SAFETY-{package}",
|
|
149
|
+
)
|
|
150
|
+
issues.append(issue)
|
|
151
|
+
|
|
152
|
+
return LinterResult(
|
|
153
|
+
linter=self.name,
|
|
154
|
+
success=returncode == 0 or len(issues) == 0,
|
|
155
|
+
issues=issues,
|
|
156
|
+
errors=errors,
|
|
157
|
+
execution_time=execution_time,
|
|
158
|
+
raw_output=output,
|
|
159
|
+
)
|
|
160
|
+
except json.JSONDecodeError:
|
|
161
|
+
# Safety might output text format
|
|
162
|
+
if output.strip():
|
|
163
|
+
for line in output.split("\n"):
|
|
164
|
+
if line.strip() and "is vulnerable" in line.lower():
|
|
165
|
+
severity = Severity.HIGH
|
|
166
|
+
if "critical" in line.lower():
|
|
167
|
+
severity = Severity.CRITICAL
|
|
168
|
+
|
|
169
|
+
issue = LinterIssue(
|
|
170
|
+
linter=self.name,
|
|
171
|
+
severity=severity,
|
|
172
|
+
message=line.strip(),
|
|
173
|
+
file="requirements.txt",
|
|
174
|
+
)
|
|
175
|
+
issues.append(issue)
|
|
176
|
+
|
|
177
|
+
return LinterResult(
|
|
178
|
+
linter=self.name,
|
|
179
|
+
success=returncode == 0 or len(issues) == 0,
|
|
180
|
+
issues=issues,
|
|
181
|
+
errors=errors,
|
|
182
|
+
execution_time=execution_time,
|
|
183
|
+
raw_output=output,
|
|
184
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Reflection engine for CodeOptiX."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from codeoptix.artifacts.manager import ArtifactManager
|
|
6
|
+
from codeoptix.reflection.generator import ReflectionGenerator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ReflectionEngine:
|
|
10
|
+
"""
|
|
11
|
+
Reflection engine that analyzes evaluation results and generates insights.
|
|
12
|
+
|
|
13
|
+
Generates human-readable reflection reports identifying root causes
|
|
14
|
+
and providing actionable recommendations.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self, artifact_manager: ArtifactManager | None = None, config: dict[str, Any] | None = None
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Initialize reflection engine.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
artifact_manager: Artifact manager for loading/saving
|
|
25
|
+
config: Configuration dictionary
|
|
26
|
+
"""
|
|
27
|
+
self.artifact_manager = artifact_manager or ArtifactManager()
|
|
28
|
+
self.config = config or {}
|
|
29
|
+
self.generator = ReflectionGenerator(self.config.get("generator", {}))
|
|
30
|
+
|
|
31
|
+
def reflect(
|
|
32
|
+
self, results: dict[str, Any], agent_name: str | None = None, save: bool = True
|
|
33
|
+
) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Generate reflection from evaluation results.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
results: Evaluation results dictionary
|
|
39
|
+
agent_name: Optional agent name
|
|
40
|
+
save: Whether to save reflection to file
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Reflection markdown content
|
|
44
|
+
"""
|
|
45
|
+
# Generate reflection content
|
|
46
|
+
reflection_content = self.generator.generate(results, agent_name=agent_name)
|
|
47
|
+
|
|
48
|
+
# Save if requested
|
|
49
|
+
if save:
|
|
50
|
+
run_id = results.get("run_id")
|
|
51
|
+
self.artifact_manager.save_reflection(reflection_content, run_id=run_id)
|
|
52
|
+
|
|
53
|
+
return reflection_content
|
|
54
|
+
|
|
55
|
+
def reflect_from_run_id(self, run_id: str, agent_name: str | None = None) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Generate reflection from a previous evaluation run.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
run_id: Run ID to load results from
|
|
61
|
+
agent_name: Optional agent name
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Reflection markdown content
|
|
65
|
+
"""
|
|
66
|
+
# Load results
|
|
67
|
+
results = self.artifact_manager.load_results(run_id)
|
|
68
|
+
|
|
69
|
+
# Generate reflection
|
|
70
|
+
return self.reflect(results, agent_name=agent_name, save=True)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Reflection content generation for CodeOptiX."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ReflectionGenerator:
|
|
7
|
+
"""Generates human-readable reflection reports from evaluation results."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, config: dict[str, Any] | None = None):
|
|
10
|
+
"""Initialize reflection generator."""
|
|
11
|
+
self.config = config or {}
|
|
12
|
+
|
|
13
|
+
def generate(self, results: dict[str, Any], agent_name: str | None = None) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Generate reflection markdown from evaluation results.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
results: Evaluation results dictionary
|
|
19
|
+
agent_name: Optional agent name
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Reflection markdown content
|
|
23
|
+
"""
|
|
24
|
+
run_id = results.get("run_id", "unknown")
|
|
25
|
+
timestamp = results.get("timestamp", "unknown")
|
|
26
|
+
overall_score = results.get("overall_score", 0.0)
|
|
27
|
+
behaviors = results.get("behaviors", {})
|
|
28
|
+
|
|
29
|
+
lines = []
|
|
30
|
+
lines.append("# CodeOptiX Reflection Report\n")
|
|
31
|
+
lines.append(f"**Run ID**: {run_id} \n")
|
|
32
|
+
lines.append(f"**Timestamp**: {timestamp} \n")
|
|
33
|
+
if agent_name:
|
|
34
|
+
lines.append(f"**Agent**: {agent_name} \n")
|
|
35
|
+
lines.append(f"**Overall Score**: {overall_score:.2f}/1.0 \n")
|
|
36
|
+
lines.append("\n---\n")
|
|
37
|
+
|
|
38
|
+
# Summary section
|
|
39
|
+
lines.append("## Summary\n\n")
|
|
40
|
+
total_behaviors = len(behaviors)
|
|
41
|
+
passed_behaviors = sum(1 for b in behaviors.values() if b.get("scenarios_passed", 0) > 0)
|
|
42
|
+
failed_behaviors = total_behaviors - passed_behaviors
|
|
43
|
+
|
|
44
|
+
lines.append(f"- **Total Behaviors Evaluated**: {total_behaviors}\n")
|
|
45
|
+
lines.append(f"- **Behaviors Passed**: {passed_behaviors}\n")
|
|
46
|
+
lines.append(f"- **Behaviors Failed**: {failed_behaviors}\n")
|
|
47
|
+
lines.append(f"- **Overall Score**: {overall_score:.2f}/1.0\n")
|
|
48
|
+
lines.append("\n")
|
|
49
|
+
|
|
50
|
+
# Behavior analysis section
|
|
51
|
+
lines.append("## Behavior Analysis\n\n")
|
|
52
|
+
|
|
53
|
+
for behavior_name, behavior_data in behaviors.items():
|
|
54
|
+
lines.append(f"### {behavior_name}\n\n")
|
|
55
|
+
|
|
56
|
+
score = behavior_data.get("score", 0.0)
|
|
57
|
+
scenarios_tested = behavior_data.get("scenarios_tested", 0)
|
|
58
|
+
scenarios_passed = behavior_data.get("scenarios_passed", 0)
|
|
59
|
+
scenario_results = behavior_data.get("scenario_results", [])
|
|
60
|
+
severity = (
|
|
61
|
+
scenario_results[0].get("behavior_result", {}).get("severity", "medium")
|
|
62
|
+
if scenario_results
|
|
63
|
+
else "medium"
|
|
64
|
+
)
|
|
65
|
+
evidence = behavior_data.get("evidence", [])
|
|
66
|
+
|
|
67
|
+
# Status badge
|
|
68
|
+
status = "✅ PASSED" if scenarios_passed > 0 else "❌ FAILED"
|
|
69
|
+
lines.append(f"**Status**: {status} \n")
|
|
70
|
+
lines.append(f"**Score**: {score:.2f}/1.0 \n")
|
|
71
|
+
lines.append(f"**Severity**: {severity.upper()} \n")
|
|
72
|
+
lines.append(f"**Scenarios Tested**: {scenarios_tested} \n")
|
|
73
|
+
lines.append(f"**Scenarios Passed**: {scenarios_passed} \n")
|
|
74
|
+
lines.append("\n")
|
|
75
|
+
|
|
76
|
+
# Evidence
|
|
77
|
+
if evidence:
|
|
78
|
+
lines.append("**Evidence**:\n")
|
|
79
|
+
for ev in evidence[:10]: # Limit to 10 items
|
|
80
|
+
lines.append(f"- {ev}\n")
|
|
81
|
+
if len(evidence) > 10:
|
|
82
|
+
lines.append(f"- ... and {len(evidence) - 10} more issues\n")
|
|
83
|
+
lines.append("\n")
|
|
84
|
+
|
|
85
|
+
# Root cause analysis
|
|
86
|
+
root_causes = self._identify_root_causes(behavior_data)
|
|
87
|
+
if root_causes:
|
|
88
|
+
lines.append("**Root Causes**:\n")
|
|
89
|
+
for cause in root_causes:
|
|
90
|
+
lines.append(f"- {cause}\n")
|
|
91
|
+
lines.append("\n")
|
|
92
|
+
|
|
93
|
+
# Recommendations
|
|
94
|
+
recommendations = self._generate_recommendations(behavior_name, behavior_data)
|
|
95
|
+
if recommendations:
|
|
96
|
+
lines.append("**Recommendations**:\n")
|
|
97
|
+
for rec in recommendations:
|
|
98
|
+
lines.append(f"- {rec}\n")
|
|
99
|
+
lines.append("\n")
|
|
100
|
+
|
|
101
|
+
lines.append("---\n\n")
|
|
102
|
+
|
|
103
|
+
# Overall recommendations
|
|
104
|
+
lines.append("## Overall Recommendations\n\n")
|
|
105
|
+
overall_recs = self._generate_overall_recommendations(results)
|
|
106
|
+
for rec in overall_recs:
|
|
107
|
+
lines.append(f"- {rec}\n")
|
|
108
|
+
lines.append("\n")
|
|
109
|
+
|
|
110
|
+
# Metadata
|
|
111
|
+
lines.append("## Metadata\n\n")
|
|
112
|
+
metadata = results.get("metadata", {})
|
|
113
|
+
for key, value in metadata.items():
|
|
114
|
+
lines.append(f"- **{key}**: {value}\n")
|
|
115
|
+
|
|
116
|
+
return "".join(lines)
|
|
117
|
+
|
|
118
|
+
def _identify_root_causes(self, behavior_data: dict[str, Any]) -> list[str]:
|
|
119
|
+
"""Identify root causes from behavior evaluation."""
|
|
120
|
+
causes = []
|
|
121
|
+
|
|
122
|
+
evidence = behavior_data.get("evidence", [])
|
|
123
|
+
scenario_results = behavior_data.get("scenario_results", [])
|
|
124
|
+
|
|
125
|
+
# Analyze evidence patterns
|
|
126
|
+
if any("hardcoded" in ev.lower() for ev in evidence):
|
|
127
|
+
causes.append("Missing security guidelines in agent prompt")
|
|
128
|
+
|
|
129
|
+
if any("no assertions" in ev.lower() for ev in evidence):
|
|
130
|
+
causes.append("Insufficient test quality requirements in prompt")
|
|
131
|
+
|
|
132
|
+
if any("missing" in ev.lower() for ev in evidence):
|
|
133
|
+
causes.append("Agent not following planning artifacts")
|
|
134
|
+
|
|
135
|
+
# Check evaluator results
|
|
136
|
+
for scenario_result in scenario_results:
|
|
137
|
+
eval_results = scenario_result.get("evaluator_results", {})
|
|
138
|
+
|
|
139
|
+
if eval_results.get("static_analysis", {}).get("bandit"):
|
|
140
|
+
bandit_data = eval_results["static_analysis"]["bandit"]
|
|
141
|
+
if (
|
|
142
|
+
bandit_data
|
|
143
|
+
and bandit_data.get("metrics", {}).get("_totals", {}).get("SEVERITY.HIGH", 0)
|
|
144
|
+
> 0
|
|
145
|
+
):
|
|
146
|
+
causes.append("Static analysis detected security vulnerabilities")
|
|
147
|
+
|
|
148
|
+
if eval_results.get("test_execution", {}).get("failed_count", 0) > 0:
|
|
149
|
+
causes.append("Tests are failing or have low coverage")
|
|
150
|
+
|
|
151
|
+
return list(set(causes)) # Remove duplicates
|
|
152
|
+
|
|
153
|
+
def _generate_recommendations(
|
|
154
|
+
self, behavior_name: str, behavior_data: dict[str, Any]
|
|
155
|
+
) -> list[str]:
|
|
156
|
+
"""Generate recommendations for a specific behavior."""
|
|
157
|
+
recommendations = []
|
|
158
|
+
score = behavior_data.get("score", 0.0)
|
|
159
|
+
evidence = behavior_data.get("evidence", [])
|
|
160
|
+
|
|
161
|
+
if behavior_name == "insecure-code":
|
|
162
|
+
if score < 0.7:
|
|
163
|
+
recommendations.append("Add security checklist to agent prompt")
|
|
164
|
+
recommendations.append("Include examples of secure code patterns")
|
|
165
|
+
recommendations.append("Add explicit guidance against hardcoded secrets")
|
|
166
|
+
|
|
167
|
+
elif behavior_name == "vacuous-tests":
|
|
168
|
+
if score < 0.7:
|
|
169
|
+
recommendations.append("Emphasize test quality requirements in prompt")
|
|
170
|
+
recommendations.append("Include examples of meaningful test assertions")
|
|
171
|
+
recommendations.append("Add guidance on test coverage expectations")
|
|
172
|
+
|
|
173
|
+
elif behavior_name == "plan-drift":
|
|
174
|
+
if score < 0.7:
|
|
175
|
+
recommendations.append("Strengthen alignment with planning artifacts")
|
|
176
|
+
recommendations.append("Add explicit requirement tracking in prompt")
|
|
177
|
+
recommendations.append("Include examples of plan-following behavior")
|
|
178
|
+
|
|
179
|
+
# General recommendations based on evidence
|
|
180
|
+
if len(evidence) > 5:
|
|
181
|
+
recommendations.append(
|
|
182
|
+
"Multiple issues detected - consider comprehensive prompt revision"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return recommendations
|
|
186
|
+
|
|
187
|
+
def _generate_overall_recommendations(self, results: dict[str, Any]) -> list[str]:
|
|
188
|
+
"""Generate overall recommendations."""
|
|
189
|
+
recommendations = []
|
|
190
|
+
overall_score = results.get("overall_score", 0.0)
|
|
191
|
+
behaviors = results.get("behaviors", {})
|
|
192
|
+
|
|
193
|
+
if overall_score < 0.5:
|
|
194
|
+
recommendations.append("**Critical**: Agent performance is below acceptable threshold")
|
|
195
|
+
recommendations.append("Consider major prompt revision or agent configuration changes")
|
|
196
|
+
elif overall_score < 0.7:
|
|
197
|
+
recommendations.append("**Warning**: Agent performance needs improvement")
|
|
198
|
+
recommendations.append("Review and update agent prompts based on evidence")
|
|
199
|
+
else:
|
|
200
|
+
recommendations.append("Agent performance is acceptable but can be improved")
|
|
201
|
+
|
|
202
|
+
# Behavior-specific recommendations
|
|
203
|
+
failed_behaviors = [
|
|
204
|
+
name for name, data in behaviors.items() if data.get("score", 0.0) < 0.7
|
|
205
|
+
]
|
|
206
|
+
if failed_behaviors:
|
|
207
|
+
recommendations.append(f"Focus on improving: {', '.join(failed_behaviors)}")
|
|
208
|
+
|
|
209
|
+
return recommendations
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility modules for CodeOptix."""
|