Zhen Ye Claude Opus 4.6 (1M context) commited on
Commit
6dcbca5
Β·
1 Parent(s): c382b55

fix: graph layout spacing + validator API fixes

Browse files

- Use d3.tree().nodeSize() for guaranteed 100px spacing between leaves
- Center tree with xOffset calculation, horizontal scroll for narrow panels
- Fix Gemini SDK: use google-genai client API (types.Part.from_bytes)
- Add fuzzy key matching in merge (case-insensitive + feature-name fallback)
- Add response logging for Claude and Gemini validators
- Update requirements: google-genai replaces google-generativeai

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. demo/index.html +21 -10
  2. models/isr/explainer.py +38 -12
  3. requirements.txt +1 -1
demo/index.html CHANGED
@@ -1771,13 +1771,14 @@
1771
 
1772
  /* ── Explainability Graph ─────────────────────────────────────── */
1773
  #explainPanel {
1774
- overflow-y: auto;
1775
  padding: 0;
 
1776
  }
1777
 
1778
  .explain-svg {
1779
  display: block;
1780
- width: 100%;
1781
  font-family: 'Inter', -apple-system, sans-serif;
1782
  }
1783
  .explain-svg text {
@@ -3450,13 +3451,15 @@
3450
  })),
3451
  };
3452
 
3453
- const margin = { top: 30, right: 20, bottom: 60, left: 20 };
3454
- const width = container.clientWidth || 340;
3455
  const totalLeaves = root.children.reduce((s, c) => s + (c.children ? c.children.length : 1), 0);
3456
- const height = Math.max(280, totalLeaves * 35 + 120);
 
 
 
3457
 
3458
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3459
- svg.setAttribute('width', width);
3460
  svg.setAttribute('height', height);
3461
  svg.setAttribute('class', 'explain-svg');
3462
  container.appendChild(svg);
@@ -3466,11 +3469,18 @@
3466
  defs.innerHTML = '<filter id="exGlow"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>';
3467
  svg.appendChild(defs);
3468
 
3469
- // D3 layout
3470
  const hierarchy = d3.hierarchy(root);
3471
- const treeLayout = d3.tree().size([width - margin.left - margin.right, height - margin.top - margin.bottom]);
3472
  treeLayout(hierarchy);
3473
 
 
 
 
 
 
 
 
3474
  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3475
  g.setAttribute('transform', `translate(${margin.left},${margin.top})`);
3476
  svg.appendChild(g);
@@ -3478,7 +3488,8 @@
3478
  // Edges
3479
  hierarchy.links().forEach(link => {
3480
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3481
- const sx = link.source.x, sy = link.source.y, tx = link.target.x, ty = link.target.y;
 
3482
  const my = (sy + ty) / 2;
3483
  path.setAttribute('d', `M${sx},${sy} C${sx},${my} ${tx},${my} ${tx},${ty}`);
3484
  path.setAttribute('fill', 'none');
@@ -3493,7 +3504,7 @@
3493
  hierarchy.descendants().forEach(node => {
3494
  const d = node.data;
3495
  const ng = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3496
- ng.setAttribute('transform', `translate(${node.x},${node.y})`);
3497
 
3498
  if (node.depth === 0) {
3499
  drawExRoot(ng, d);
 
1771
 
1772
  /* ── Explainability Graph ─────────────────────────────────────── */
1773
  #explainPanel {
1774
+ overflow: auto;
1775
  padding: 0;
1776
+ position: relative;
1777
  }
1778
 
1779
  .explain-svg {
1780
  display: block;
1781
+ min-width: 100%;
1782
  font-family: 'Inter', -apple-system, sans-serif;
1783
  }
1784
  .explain-svg text {
 
3451
  })),
3452
  };
3453
 
3454
+ const margin = { top: 30, right: 50, bottom: 60, left: 50 };
 
3455
  const totalLeaves = root.children.reduce((s, c) => s + (c.children ? c.children.length : 1), 0);
3456
+ const nodeSpacingX = 100; // min horizontal space per leaf
3457
+ const levelHeight = 90; // vertical space between levels
3458
+ const minWidth = Math.max(container.clientWidth || 340, totalLeaves * nodeSpacingX + margin.left + margin.right);
3459
+ const height = Math.max(280, 3 * levelHeight + margin.top + margin.bottom);
3460
 
3461
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3462
+ svg.setAttribute('width', minWidth);
3463
  svg.setAttribute('height', height);
3464
  svg.setAttribute('class', 'explain-svg');
3465
  container.appendChild(svg);
 
3469
  defs.innerHTML = '<filter id="exGlow"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>';
3470
  svg.appendChild(defs);
3471
 
3472
+ // D3 layout β€” use nodeSize for guaranteed spacing
3473
  const hierarchy = d3.hierarchy(root);
3474
+ const treeLayout = d3.tree().nodeSize([nodeSpacingX, levelHeight]);
3475
  treeLayout(hierarchy);
3476
 
3477
+ // Shift all x positions so the tree is centered (nodeSize uses 0-centered coords)
3478
+ const allX = hierarchy.descendants().map(n => n.x);
3479
+ const xMin = Math.min(...allX);
3480
+ const xMax = Math.max(...allX);
3481
+ const treeWidth = xMax - xMin;
3482
+ const xOffset = (minWidth - margin.left - margin.right) / 2 - (xMin + treeWidth / 2);
3483
+
3484
  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3485
  g.setAttribute('transform', `translate(${margin.left},${margin.top})`);
3486
  svg.appendChild(g);
 
3488
  // Edges
3489
  hierarchy.links().forEach(link => {
3490
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3491
+ const sx = link.source.x + xOffset, sy = link.source.y;
3492
+ const tx = link.target.x + xOffset, ty = link.target.y;
3493
  const my = (sy + ty) / 2;
3494
  path.setAttribute('d', `M${sx},${sy} C${sx},${my} ${tx},${my} ${tx},${ty}`);
3495
  path.setAttribute('fill', 'none');
 
3504
  hierarchy.descendants().forEach(node => {
3505
  const d = node.data;
3506
  const ng = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3507
+ ng.setAttribute('transform', `translate(${node.x + xOffset},${node.y})`);
3508
 
3509
  if (node.depth === 0) {
3510
  drawExRoot(ng, d);
models/isr/explainer.py CHANGED
@@ -218,6 +218,7 @@ class ISRExplainer:
218
  )
219
 
220
  raw = response.content[0].text
 
221
  return parse_llm_json(raw)
222
  except Exception:
223
  logger.exception("Claude validation failed")
@@ -232,10 +233,10 @@ class ISRExplainer:
232
 
233
  try:
234
  import base64
235
- import google.generativeai as genai
 
236
 
237
- genai.configure(api_key=api_key)
238
- model = genai.GenerativeModel("gemini-2.0-flash")
239
 
240
  user_text = self._build_metadata_text(metadata, mission)
241
  user_text += f"\n\nPrimary analyst's feature tree:\n```json\n{json.dumps(tree, indent=2)}\n```"
@@ -245,20 +246,24 @@ class ISRExplainer:
245
  frame_bytes = base64.b64decode(frame_b64)
246
 
247
  response = await asyncio.to_thread(
248
- model.generate_content,
249
- [
250
- _VALIDATOR_SYSTEM_PROMPT + "\n\n" + user_text,
251
- {"mime_type": "image/jpeg", "data": crop_bytes},
252
- "\n[Full frame context]:",
253
- {"mime_type": "image/jpeg", "data": frame_bytes},
 
 
 
254
  ],
255
- generation_config=genai.GenerationConfig(
256
  max_output_tokens=1024,
257
  temperature=0.3,
258
  ),
259
  )
260
 
261
  raw = response.text
 
262
  return parse_llm_json(raw)
263
  except Exception:
264
  logger.exception("Gemini validation failed")
@@ -302,14 +307,14 @@ class ISRExplainer:
302
  feat_agreed = 0
303
 
304
  if claude and "feature_validations" in claude:
305
- cv = claude["feature_validations"].get(feat_key)
306
  if cv:
307
  validators["claude"] = cv
308
  if cv.get("agree"):
309
  feat_agreed += 1
310
 
311
  if gemini and "feature_validations" in gemini:
312
- gv = gemini["feature_validations"].get(feat_key)
313
  if gv:
314
  validators["gemini"] = gv
315
  if gv.get("agree"):
@@ -329,3 +334,24 @@ class ISRExplainer:
329
  }
330
 
331
  return tree
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  )
219
 
220
  raw = response.content[0].text
221
+ logger.info("Claude raw response: %s", raw[:200] if raw else "empty")
222
  return parse_llm_json(raw)
223
  except Exception:
224
  logger.exception("Claude validation failed")
 
233
 
234
  try:
235
  import base64
236
+ from google import genai
237
+ from google.genai import types
238
 
239
+ client = genai.Client(api_key=api_key)
 
240
 
241
  user_text = self._build_metadata_text(metadata, mission)
242
  user_text += f"\n\nPrimary analyst's feature tree:\n```json\n{json.dumps(tree, indent=2)}\n```"
 
246
  frame_bytes = base64.b64decode(frame_b64)
247
 
248
  response = await asyncio.to_thread(
249
+ client.models.generate_content,
250
+ model="gemini-2.0-flash",
251
+ contents=[
252
+ types.Content(role="user", parts=[
253
+ types.Part.from_text(_VALIDATOR_SYSTEM_PROMPT + "\n\n" + user_text),
254
+ types.Part.from_bytes(data=crop_bytes, mime_type="image/jpeg"),
255
+ types.Part.from_text("\n[Full frame context]:"),
256
+ types.Part.from_bytes(data=frame_bytes, mime_type="image/jpeg"),
257
+ ]),
258
  ],
259
+ config=types.GenerateContentConfig(
260
  max_output_tokens=1024,
261
  temperature=0.3,
262
  ),
263
  )
264
 
265
  raw = response.text
266
+ logger.info("Gemini raw response: %s", raw[:200] if raw else "empty")
267
  return parse_llm_json(raw)
268
  except Exception:
269
  logger.exception("Gemini validation failed")
 
307
  feat_agreed = 0
308
 
309
  if claude and "feature_validations" in claude:
310
+ cv = self._find_validation(claude["feature_validations"], feat_key, feat["name"])
311
  if cv:
312
  validators["claude"] = cv
313
  if cv.get("agree"):
314
  feat_agreed += 1
315
 
316
  if gemini and "feature_validations" in gemini:
317
+ gv = self._find_validation(gemini["feature_validations"], feat_key, feat["name"])
318
  if gv:
319
  validators["gemini"] = gv
320
  if gv.get("agree"):
 
334
  }
335
 
336
  return tree
337
+
338
+ @staticmethod
339
+ def _find_validation(validations: dict, exact_key: str, feat_name: str) -> Optional[dict]:
340
+ """Find validation by exact key first, then fuzzy match on feature name."""
341
+ # Exact match
342
+ val = validations.get(exact_key)
343
+ if val:
344
+ return val
345
+ # Fuzzy: try case-insensitive exact key
346
+ lower_key = exact_key.lower()
347
+ for k, v in validations.items():
348
+ if k.lower() == lower_key:
349
+ return v
350
+ # Fuzzy: match by feature name alone (validator may omit category)
351
+ lower_name = feat_name.lower()
352
+ for k, v in validations.items():
353
+ parts = k.split("/")
354
+ candidate = parts[-1].lower() if parts else k.lower()
355
+ if candidate == lower_name:
356
+ return v
357
+ return None
requirements.txt CHANGED
@@ -17,4 +17,4 @@ psutil
17
  dill
18
  openai>=1.0.0
19
  anthropic>=0.40.0
20
- google-generativeai>=0.8.0
 
17
  dill
18
  openai>=1.0.0
19
  anthropic>=0.40.0
20
+ google-genai>=1.0.0