differix 0.1.0a1__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.
- agents/editor.py +455 -0
- agents/researcher.py +405 -0
- agents/reviewer.py +374 -0
- cli/model_cli.py +430 -0
- config.py +557 -0
- differix-0.1.0a1.dist-info/METADATA +123 -0
- differix-0.1.0a1.dist-info/RECORD +62 -0
- differix-0.1.0a1.dist-info/WHEEL +5 -0
- differix-0.1.0a1.dist-info/entry_points.txt +2 -0
- differix-0.1.0a1.dist-info/top_level.txt +6 -0
- main.py +2796 -0
- memory/__init__.py +59 -0
- memory/embeddings/__init__.py +29 -0
- memory/embeddings/factory.py +178 -0
- memory/embeddings/providers.py +259 -0
- memory/manager.py +186 -0
- memory/migrator.py +253 -0
- memory/ranker.py +203 -0
- memory/schemas.py +127 -0
- memory/storage.py +552 -0
- tools/agents_chat.py +152 -0
- tools/asset_extractor.py +545 -0
- tools/asset_manager.py +115 -0
- tools/atomic_changeset.py +264 -0
- tools/backup_manager.py +122 -0
- tools/changelog_engine.py +261 -0
- tools/coherence_checker.py +771 -0
- tools/compiler.py +531 -0
- tools/component_normalizer.py +130 -0
- tools/context_assembler.py +631 -0
- tools/contract_validator.py +478 -0
- tools/cost_predictor.py +141 -0
- tools/drift_guard.py +196 -0
- tools/file_manager.py +119 -0
- tools/handshake.py +351 -0
- tools/lesson_manager.py +570 -0
- tools/lesson_memory_bridge.py +398 -0
- tools/llm_client.py +103 -0
- tools/llm_selector.py +362 -0
- tools/model_ranker.py +535 -0
- tools/palette_extractor.py +1043 -0
- tools/plugin_manager.py +204 -0
- tools/project_context_gatherer.py +404 -0
- tools/project_scanner.py +41 -0
- tools/project_structure.py +244 -0
- tools/reasoning_logger.py +78 -0
- tools/reflection.py +452 -0
- tools/sandbox_utils.py +56 -0
- tools/security/__init__.py +0 -0
- tools/security/behavior_guard.py +83 -0
- tools/security/verification_firewall.py +113 -0
- tools/security_filter.py +213 -0
- tools/session_adr.py +311 -0
- tools/skill_manager.py +129 -0
- tools/state_manager.py +383 -0
- tools/structure_index.py +570 -0
- tools/test_schema_consistency.py +19 -0
- tools/verification_firewall.py +113 -0
- tools/visual_capture.py +65 -0
- tools/visual_diff.py +220 -0
- tools/visual_reviewer.py +79 -0
- tools/web_search.py +371 -0
agents/reviewer.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import config
|
|
5
|
+
from tools.lesson_manager import get_promoted_lessons_prompt, create_observation
|
|
6
|
+
from tools.skill_manager import get_combined_agent_skill
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from tools.llm_client import safe_chat_completion
|
|
10
|
+
except ImportError:
|
|
11
|
+
from llm_client import safe_chat_completion
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_reviewer_model() -> str:
|
|
15
|
+
if hasattr(config, "get_active_model") and callable(getattr(config, "get_active_model")):
|
|
16
|
+
return config.get_active_model("reviewer")
|
|
17
|
+
if hasattr(config, "get_agent_model") and callable(getattr(config, "get_agent_model")):
|
|
18
|
+
return config.get_agent_model("reviewer")
|
|
19
|
+
return getattr(config, "MODEL_REVIEWER", "google/gemini-2.5-pro")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_sandboxed_path(target_file: str, project_dir: str = ".") -> str:
|
|
23
|
+
base_path = os.path.abspath(project_dir)
|
|
24
|
+
target_path = os.path.abspath(os.path.join(base_path, target_file))
|
|
25
|
+
try:
|
|
26
|
+
contained = os.path.commonpath([base_path, target_path]) == base_path
|
|
27
|
+
except ValueError:
|
|
28
|
+
contained = False
|
|
29
|
+
if not contained:
|
|
30
|
+
raise PermissionError(f"SANDBOX VIOLATION: Reviewer path '{target_file}' escapes boundary.")
|
|
31
|
+
return target_path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _try_partial_json_extract(text: str) -> dict:
|
|
35
|
+
result: dict = {}
|
|
36
|
+
m = re.search(r'"is_approved"\s*:\s*(true|false)', text, re.IGNORECASE)
|
|
37
|
+
if m:
|
|
38
|
+
result["is_approved"] = m.group(1).lower() == "true"
|
|
39
|
+
m = re.search(r'"verdict"\s*:\s*"(ACCEPT|REJECT)"', text, re.IGNORECASE)
|
|
40
|
+
if m:
|
|
41
|
+
result["verdict"] = m.group(1).upper()
|
|
42
|
+
m = re.search(r'"reason"\s*:\s*"(.*)$', text, re.DOTALL)
|
|
43
|
+
if m:
|
|
44
|
+
raw = m.group(1)
|
|
45
|
+
cleaned = re.split(r'(?<!\\)"', raw, maxsplit=1)[0]
|
|
46
|
+
result["reason"] = cleaned.strip()
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_reviewer_payload(raw_text: str) -> dict:
|
|
51
|
+
if not raw_text or not isinstance(raw_text, str):
|
|
52
|
+
return {
|
|
53
|
+
"is_approved": False,
|
|
54
|
+
"verdict": "REJECT",
|
|
55
|
+
"root_cause": "Empty model output",
|
|
56
|
+
"reason": "Reviewer received empty text from LLM."
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
text = raw_text.strip()
|
|
60
|
+
|
|
61
|
+
fence_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL)
|
|
62
|
+
if fence_match:
|
|
63
|
+
try:
|
|
64
|
+
return json.loads(fence_match.group(1))
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
start = text.find("{")
|
|
69
|
+
end = text.rfind("}")
|
|
70
|
+
if start != -1 and end != -1 and end > start:
|
|
71
|
+
try:
|
|
72
|
+
return json.loads(text[start:end + 1])
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
return json.loads(text)
|
|
78
|
+
except json.JSONDecodeError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
if text.startswith("{") or text.startswith("```"):
|
|
82
|
+
partial = _try_partial_json_extract(text)
|
|
83
|
+
if "is_approved" in partial or "verdict" in partial:
|
|
84
|
+
return {
|
|
85
|
+
"is_approved": partial.get("is_approved", False),
|
|
86
|
+
"verdict": partial.get("verdict", "REJECT"),
|
|
87
|
+
"reason": partial.get("reason", text[:200]),
|
|
88
|
+
"_partial_parse": True,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
upper_text = text.upper()
|
|
92
|
+
is_accept = any(kw in upper_text for kw in [
|
|
93
|
+
"STATUS:** APPROVED",
|
|
94
|
+
"STATUS: APPROVED",
|
|
95
|
+
"VERDICT: ACCEPT",
|
|
96
|
+
"VERDICT: APPROVED",
|
|
97
|
+
'"IS_APPROVED": TRUE',
|
|
98
|
+
"\"VERDICT\": \"ACCEPT\""
|
|
99
|
+
]) or (
|
|
100
|
+
"ACCEPT" in upper_text
|
|
101
|
+
and "REJECT" not in upper_text
|
|
102
|
+
and "DO NOT ACCEPT" not in upper_text
|
|
103
|
+
and "NOT ACCEPT" not in upper_text
|
|
104
|
+
and "CANNOT ACCEPT" not in upper_text
|
|
105
|
+
and "REFUSE" not in upper_text
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
reason_match = re.search(r"\*\*Analysis:\*\*\s*(.+)", text, re.DOTALL)
|
|
109
|
+
clean_reason = reason_match.group(1).strip() if reason_match else text[:300]
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"is_approved": is_accept,
|
|
113
|
+
"verdict": "ACCEPT" if is_accept else "REJECT",
|
|
114
|
+
"root_cause": "Parsed from Markdown text response." if is_accept else "Reviewer rejected changes.",
|
|
115
|
+
"reason": clean_reason
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def run_reviewer(state, project_dir=".", session_context="", coherence_report="", visual_context=""):
|
|
119
|
+
"""
|
|
120
|
+
Run the reviewer agent.
|
|
121
|
+
|
|
122
|
+
Uses Tiered Context Assembly to bound token usage. The reviewed code
|
|
123
|
+
is rendered as a unified diff when possible (10-30x smaller than the
|
|
124
|
+
full file for small edits), falling back to a sliced view when no
|
|
125
|
+
prior content is available (new files).
|
|
126
|
+
|
|
127
|
+
Also fixes a stray `task_spec=...` no-op assignment that previously
|
|
128
|
+
appeared after the safe_chat_completion call.
|
|
129
|
+
"""
|
|
130
|
+
from tools.context_assembler import (
|
|
131
|
+
ContextBundle,
|
|
132
|
+
Tier,
|
|
133
|
+
TierItem,
|
|
134
|
+
ContextAssembler,
|
|
135
|
+
smart_slice_code,
|
|
136
|
+
dedupe_logs,
|
|
137
|
+
render_code_diff,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# ---- Lessons (only when session_context is missing) ----
|
|
141
|
+
if session_context:
|
|
142
|
+
promoted_lessons = ""
|
|
143
|
+
else:
|
|
144
|
+
from tools.lesson_manager import get_ranked_lessons_for_role
|
|
145
|
+
|
|
146
|
+
retrieval_query = " ".join(filter(None, [
|
|
147
|
+
str(state.get("active_subtask") or ""),
|
|
148
|
+
str(state.get("research_analysis") or ""),
|
|
149
|
+
str(state.get("goal") or ""),
|
|
150
|
+
str(state.get("target_file") or ""),
|
|
151
|
+
])).strip() or "code review"
|
|
152
|
+
|
|
153
|
+
_, lessons_block, _ = get_ranked_lessons_for_role(
|
|
154
|
+
role="reviewer",
|
|
155
|
+
query=retrieval_query,
|
|
156
|
+
limit=getattr(config, "LESSONS_RETRIEVAL_LIMIT", 5),
|
|
157
|
+
)
|
|
158
|
+
promoted_lessons = lessons_block or get_promoted_lessons_prompt()
|
|
159
|
+
|
|
160
|
+
iteration_logs = state.get("iteration_logs") or state.get("compiler_output", "") or ""
|
|
161
|
+
skill_prompt = get_combined_agent_skill("reviewer", project_dir)
|
|
162
|
+
|
|
163
|
+
# System prompt — STRICT JSON only
|
|
164
|
+
coherence_rule = ""
|
|
165
|
+
if coherence_report:
|
|
166
|
+
coherence_rule = (
|
|
167
|
+
"\n- If the COHERENCE REPORT below contains any ERROR-level finding, "
|
|
168
|
+
"you MUST set is_approved to false. Cross-file coherence violations "
|
|
169
|
+
"are blocking and cannot be overridden by style or completeness."
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
system_prompt = (
|
|
173
|
+
f"{skill_prompt}\n\n"
|
|
174
|
+
f"CRITICAL REQUIREMENT:\n"
|
|
175
|
+
f"You MUST respond ONLY with a raw, valid JSON object matching this "
|
|
176
|
+
f"schema:\n"
|
|
177
|
+
f'{{"is_approved": true, "verdict": "ACCEPT", "reason": "Short summary."}}\n'
|
|
178
|
+
f"Rules:\n"
|
|
179
|
+
f"- reason MUST be under 150 characters.\n"
|
|
180
|
+
f"- Do NOT write Markdown headers or prose outside the JSON.\n"
|
|
181
|
+
f"- Do NOT nest JSON inside fields.\n"
|
|
182
|
+
f"- The first character of your response MUST be {{ and the last MUST be }}."
|
|
183
|
+
f"{coherence_rule}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
compiler_log = str(state.get("compiler_output", ""))
|
|
187
|
+
target_file = state.get("active_target_file") or state.get("target_file")
|
|
188
|
+
|
|
189
|
+
generated_data = state.get("generated_code")
|
|
190
|
+
code_content = (
|
|
191
|
+
generated_data.get("code")
|
|
192
|
+
if isinstance(generated_data, dict) else generated_data
|
|
193
|
+
)
|
|
194
|
+
apply_result = (
|
|
195
|
+
generated_data.get("apply_result")
|
|
196
|
+
if isinstance(generated_data, dict) else None
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if target_file and not code_content:
|
|
200
|
+
try:
|
|
201
|
+
safe_target_path = resolve_sandboxed_path(target_file, project_dir)
|
|
202
|
+
if os.path.exists(safe_target_path):
|
|
203
|
+
with open(safe_target_path, "r", encoding="utf-8") as f:
|
|
204
|
+
code_content = f.read()
|
|
205
|
+
except PermissionError as pe:
|
|
206
|
+
code_content = f"SANDBOX REJECTION: {pe}"
|
|
207
|
+
|
|
208
|
+
# Pruned state — NO code here (code lives in T2)
|
|
209
|
+
pruned_state = {
|
|
210
|
+
"iteration": state.get("iteration_count", 0),
|
|
211
|
+
"active_subtask": state.get("active_subtask", ""),
|
|
212
|
+
"target_file": target_file,
|
|
213
|
+
}
|
|
214
|
+
if isinstance(apply_result, dict):
|
|
215
|
+
pruned_state["patch_apply_status"] = {
|
|
216
|
+
"ok": apply_result.get("ok"),
|
|
217
|
+
"mode": apply_result.get("mode"),
|
|
218
|
+
"matched": apply_result.get("matched"),
|
|
219
|
+
"failed": apply_result.get("failed"),
|
|
220
|
+
}
|
|
221
|
+
injected_ids = state.get("active_lesson_node_ids") or []
|
|
222
|
+
if injected_ids:
|
|
223
|
+
pruned_state["injected_lesson_node_ids"] = list(injected_ids)
|
|
224
|
+
|
|
225
|
+
# ---- Build TCA bundle ----
|
|
226
|
+
bundle = ContextBundle()
|
|
227
|
+
|
|
228
|
+
# T0
|
|
229
|
+
bundle.add_text(Tier.T0_STATIC, "skills", system_prompt)
|
|
230
|
+
|
|
231
|
+
# T1: pruned state (spec, never cut)
|
|
232
|
+
bundle.add_text(
|
|
233
|
+
Tier.T1_ACTION_CORE,
|
|
234
|
+
"state",
|
|
235
|
+
f"=== STATE TO REVIEW ===\n{json.dumps(pruned_state, ensure_ascii=False)}",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# T2: the code under review — DIFF if possible, else sliced code
|
|
239
|
+
if code_content:
|
|
240
|
+
old_code = state.get("old_code_backup", "") or ""
|
|
241
|
+
diff_view = render_code_diff(old_code, code_content, target_file or "file")
|
|
242
|
+
|
|
243
|
+
if diff_view:
|
|
244
|
+
bundle.add_text(Tier.T2_TARGET_SLICE, "diff", diff_view)
|
|
245
|
+
else:
|
|
246
|
+
hints = " ".join(filter(None, [
|
|
247
|
+
str(state.get("active_subtask") or ""),
|
|
248
|
+
str(state.get("research_analysis") or "")[:400],
|
|
249
|
+
]))
|
|
250
|
+
sliced = smart_slice_code(
|
|
251
|
+
code_content, target_file or "file", max_lines=200, hints=hints
|
|
252
|
+
)
|
|
253
|
+
tightly = smart_slice_code(
|
|
254
|
+
code_content, target_file or "file", max_lines=60, hints=hints
|
|
255
|
+
)
|
|
256
|
+
bundle.add(TierItem(
|
|
257
|
+
tier=Tier.T2_TARGET_SLICE,
|
|
258
|
+
key="code",
|
|
259
|
+
content=(
|
|
260
|
+
f"=== CODE TO REVIEW ({target_file}) ===\n{sliced}"
|
|
261
|
+
),
|
|
262
|
+
fallback_content=(
|
|
263
|
+
f"=== CODE TO REVIEW ({target_file}) [HEAVILY TRUNCATED] ===\n{tightly}"
|
|
264
|
+
if tightly != sliced else ""
|
|
265
|
+
),
|
|
266
|
+
optional=False,
|
|
267
|
+
))
|
|
268
|
+
|
|
269
|
+
# T3: coherence (blocking) > visual > session
|
|
270
|
+
if coherence_report:
|
|
271
|
+
bundle.add_text(
|
|
272
|
+
Tier.T3_INTERFACE,
|
|
273
|
+
"coherence",
|
|
274
|
+
"=== COHERENCE REPORT (machine-generated, authoritative) ===\n"
|
|
275
|
+
+ coherence_report,
|
|
276
|
+
)
|
|
277
|
+
if visual_context:
|
|
278
|
+
bundle.add_text(
|
|
279
|
+
Tier.T3_INTERFACE,
|
|
280
|
+
"visual",
|
|
281
|
+
"=== VISUAL REVIEW (machine-generated) ===\n" + visual_context,
|
|
282
|
+
)
|
|
283
|
+
if session_context:
|
|
284
|
+
bundle.add_text(Tier.T3_INTERFACE, "session", session_context)
|
|
285
|
+
|
|
286
|
+
# T4: lessons
|
|
287
|
+
if promoted_lessons:
|
|
288
|
+
bundle.add_text(
|
|
289
|
+
Tier.T4_ENRICHMENT,
|
|
290
|
+
"lessons",
|
|
291
|
+
f"=== RELEVANT LESSONS (A-SCORE RANKED) ===\n{promoted_lessons}",
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# T5: deduped logs
|
|
295
|
+
if iteration_logs.strip():
|
|
296
|
+
deduped = dedupe_logs(iteration_logs, max_errors=5)
|
|
297
|
+
if deduped:
|
|
298
|
+
bundle.add_text(
|
|
299
|
+
Tier.T5_FEEDBACK,
|
|
300
|
+
"logs",
|
|
301
|
+
f"=== RECENT EXECUTION LOGS (deduplicated) ===\n{deduped}",
|
|
302
|
+
optional=True,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# ---- Assemble ----
|
|
306
|
+
budget = getattr(config, "TOKEN_BUDGET_REVIEWER", 14000)
|
|
307
|
+
assembled = ContextAssembler(
|
|
308
|
+
budget=budget,
|
|
309
|
+
safety_margin=getattr(config, "TCA_SAFETY_MARGIN", 0.9),
|
|
310
|
+
).assemble(bundle)
|
|
311
|
+
|
|
312
|
+
if getattr(config, "TCA_LOG_SUMMARY", True):
|
|
313
|
+
try:
|
|
314
|
+
print(assembled.log_summary())
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
# ---- Call LLM ----
|
|
319
|
+
active_model = get_reviewer_model()
|
|
320
|
+
|
|
321
|
+
response, provider = safe_chat_completion(
|
|
322
|
+
role_or_model="reviewer",
|
|
323
|
+
messages=[
|
|
324
|
+
{"role": "system", "content": assembled.system_prompt},
|
|
325
|
+
{"role": "user", "content": assembled.user_prompt},
|
|
326
|
+
],
|
|
327
|
+
temperature=0.1,
|
|
328
|
+
max_tokens=4096,
|
|
329
|
+
task_spec=str(state.get("active_subtask") or state.get("target_file") or ""),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if not response or not getattr(response, "choices", None) or not response.choices:
|
|
333
|
+
raise RuntimeError("Reviewer: Empty response from model.")
|
|
334
|
+
|
|
335
|
+
result_text = (response.choices[0].message.content or "").strip()
|
|
336
|
+
result = parse_reviewer_payload(result_text)
|
|
337
|
+
|
|
338
|
+
raw_approved = result.get("is_approved")
|
|
339
|
+
if isinstance(raw_approved, str):
|
|
340
|
+
is_approved = raw_approved.lower() in ("true", "1", "yes")
|
|
341
|
+
elif isinstance(raw_approved, bool):
|
|
342
|
+
is_approved = raw_approved
|
|
343
|
+
else:
|
|
344
|
+
is_approved = str(result.get("verdict", "")).upper() == "ACCEPT"
|
|
345
|
+
|
|
346
|
+
result["is_approved"] = is_approved
|
|
347
|
+
result["verdict"] = "ACCEPT" if is_approved else "REJECT"
|
|
348
|
+
|
|
349
|
+
if not result.get("reason"):
|
|
350
|
+
result["reason"] = (
|
|
351
|
+
result.get("root_cause")
|
|
352
|
+
or result.get("fix_instruction")
|
|
353
|
+
or ("Edits approved successfully." if is_approved else "Edits rejected by reviewer.")
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if not is_approved and result.get("create_observation"):
|
|
357
|
+
obs_file = create_observation(
|
|
358
|
+
root_cause=result.get("root_cause", "Reviewer rejected implementation"),
|
|
359
|
+
details={"fix_instruction": result.get("fix_instruction", "")},
|
|
360
|
+
)
|
|
361
|
+
print(f"[Lesson Manager] Observation created by Reviewer: {obs_file}")
|
|
362
|
+
|
|
363
|
+
state["review_result"] = result
|
|
364
|
+
|
|
365
|
+
usage = getattr(response, "usage", None)
|
|
366
|
+
usage_meta = type("Meta", (), {
|
|
367
|
+
"prompt_token_count": getattr(usage, "prompt_tokens", 0) or 0,
|
|
368
|
+
"candidates_token_count": getattr(usage, "completion_tokens", 0) or 0,
|
|
369
|
+
"raw_response": result_text,
|
|
370
|
+
"provider": provider,
|
|
371
|
+
"model_id": active_model,
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
return state, usage_meta
|