Spaces:
Paused
Paused
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>
- demo/index.html +21 -10
- models/isr/explainer.py +38 -12
- requirements.txt +1 -1
demo/index.html
CHANGED
|
@@ -1771,13 +1771,14 @@
|
|
| 1771 |
|
| 1772 |
/* ββ Explainability Graph βββββββββββββββββββββββββββββββββββββββ */
|
| 1773 |
#explainPanel {
|
| 1774 |
-
overflow
|
| 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:
|
| 3454 |
-
const width = container.clientWidth || 340;
|
| 3455 |
const totalLeaves = root.children.reduce((s, c) => s + (c.children ? c.children.length : 1), 0);
|
| 3456 |
-
const
|
|
|
|
|
|
|
|
|
|
| 3457 |
|
| 3458 |
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
| 3459 |
-
svg.setAttribute('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().
|
| 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
|
|
|
|
| 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 |
-
|
|
|
|
| 236 |
|
| 237 |
-
genai.
|
| 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 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
| 254 |
],
|
| 255 |
-
|
| 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"]
|
| 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"]
|
| 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-
|
|
|
|
| 17 |
dill
|
| 18 |
openai>=1.0.0
|
| 19 |
anthropic>=0.40.0
|
| 20 |
+
google-genai>=1.0.0
|