Fix Bug 3: Filter ghost agents from reasoning tree, add MACRO_DSLR to modality label map, add debug logging for agent scores
Browse files
app.py
CHANGED
|
@@ -50,6 +50,9 @@ def run_all_agents(img: Image.Image) -> Tuple[List[AgentEvidence], ForensicVerdi
|
|
| 50 |
from agents.modality_detector import detect_modality
|
| 51 |
modality = detect_modality(img)
|
| 52 |
adj = modality.score_adjustments
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
# Signal processing agents (fast, run in parallel with modality adjustments)
|
| 55 |
signal_agents = [
|
|
@@ -78,6 +81,7 @@ def run_all_agents(img: Image.Image) -> Tuple[List[AgentEvidence], ForensicVerdi
|
|
| 78 |
try:
|
| 79 |
results[name] = future.result()
|
| 80 |
except Exception as e:
|
|
|
|
| 81 |
results[name] = AgentEvidence(
|
| 82 |
agent_name=f"{name.title()} Agent (Error)",
|
| 83 |
violation_score=0.0,
|
|
@@ -97,9 +101,25 @@ def run_all_agents(img: Image.Image) -> Tuple[List[AgentEvidence], ForensicVerdi
|
|
| 97 |
results.get("text"),
|
| 98 |
]
|
| 99 |
ordered = [r for r in ordered if r is not None]
|
| 100 |
-
|
| 101 |
-
#
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
# Attach modality info to verdict for reporting β include ALL indicators for diagnostics
|
| 105 |
verdict.reasoning_tree["modality"] = {
|
|
@@ -109,14 +129,22 @@ def run_all_agents(img: Image.Image) -> Tuple[List[AgentEvidence], ForensicVerdi
|
|
| 109 |
if not isinstance(v, np.ndarray) and k != "modality_scores"},
|
| 110 |
"modality_scores": modality.indicators.get("modality_scores", {}),
|
| 111 |
"adjustments_applied": len(modality.score_adjustments),
|
| 112 |
-
"adjustments_list": list(modality.score_adjustments.keys())[:10],
|
| 113 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
# Generate explanations
|
| 116 |
verdict.forensic_report = generate_forensic_report(verdict)
|
| 117 |
reasoning = generate_reasoning_tree(verdict)
|
| 118 |
verdict.court_brief = generate_court_brief(verdict)
|
| 119 |
|
|
|
|
| 120 |
return ordered, verdict, reasoning
|
| 121 |
|
| 122 |
|
|
@@ -174,7 +202,6 @@ def create_radar_chart(agent_results: List[AgentEvidence]) -> go.Figure:
|
|
| 174 |
for agent in agent_results:
|
| 175 |
short_name = agent.agent_name.replace(" Agent", "").replace(" Characteristics", "")
|
| 176 |
names.append(short_name)
|
| 177 |
-
# Map -1..+1 to 0..100 for display
|
| 178 |
display_score = (agent.violation_score + 1) * 50
|
| 179 |
scores.append(display_score)
|
| 180 |
if agent.violation_score > 0.2:
|
|
@@ -184,7 +211,6 @@ def create_radar_chart(agent_results: List[AgentEvidence]) -> go.Figure:
|
|
| 184 |
else:
|
| 185 |
colors.append("gold")
|
| 186 |
|
| 187 |
-
# Close the polygon
|
| 188 |
names_closed = names + [names[0]]
|
| 189 |
scores_closed = scores + [scores[0]]
|
| 190 |
|
|
@@ -199,7 +225,6 @@ def create_radar_chart(agent_results: List[AgentEvidence]) -> go.Figure:
|
|
| 199 |
name="Violation Score",
|
| 200 |
))
|
| 201 |
|
| 202 |
-
# Add neutral line at 50 (score = 0)
|
| 203 |
fig.add_trace(go.Scatterpolar(
|
| 204 |
r=[50] * (len(names) + 1),
|
| 205 |
theta=names_closed,
|
|
@@ -297,7 +322,6 @@ def create_fft_display(agent_results: List[AgentEvidence]) -> go.Figure:
|
|
| 297 |
yaxis=dict(showticklabels=False, scaleanchor="x"),
|
| 298 |
)
|
| 299 |
return fig
|
| 300 |
-
# Return empty figure
|
| 301 |
fig = go.Figure()
|
| 302 |
fig.update_layout(height=400, title="FFT Spectrum (not available)")
|
| 303 |
return fig
|
|
@@ -431,9 +455,18 @@ def analyze_image(img):
|
|
| 431 |
mod_name = mod_info.get("detected", "UNKNOWN")
|
| 432 |
mod_conf = mod_info.get("confidence", 0)
|
| 433 |
mod_adj = mod_info.get("adjustments_applied", 0)
|
| 434 |
-
mod_label = {
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
verdict_html = f"""
|
| 439 |
<div style="background:{bg}; color:white; padding:24px; border-radius:16px;
|
|
@@ -442,7 +475,7 @@ def analyze_image(img):
|
|
| 442 |
<div style="font-size:28px; font-weight:700; margin-bottom:4px;">{verdict.verdict}</div>
|
| 443 |
<div style="font-size:42px; font-weight:800;">{prob:.1%}</div>
|
| 444 |
<div style="font-size:14px; opacity:0.9; margin-top:4px;">
|
| 445 |
-
Confidence: {verdict.confidence} | Agents: {
|
| 446 |
</div>
|
| 447 |
<div style="font-size:12px; opacity:0.7; margin-top:6px; border-top:1px solid rgba(255,255,255,0.2); padding-top:6px;">
|
| 448 |
Modality: {mod_label} ({mod_conf:.0%}) | {mod_adj} test(s) recalibrated
|
|
@@ -476,6 +509,8 @@ def analyze_image(img):
|
|
| 476 |
_build_agent_details_md(agent_results),
|
| 477 |
)
|
| 478 |
except Exception as e:
|
|
|
|
|
|
|
| 479 |
error_html = f"""
|
| 480 |
<div style="background:linear-gradient(135deg, #6c757d, #495057); color:white;
|
| 481 |
padding:24px; border-radius:16px; text-align:center;">
|
|
@@ -518,7 +553,12 @@ def _build_agent_details_md(agent_results: List[AgentEvidence]) -> str:
|
|
| 518 |
note = sf.get("note", "")
|
| 519 |
s = sf.get("score", 0)
|
| 520 |
ic = "π΄" if s > 0.2 else "π’" if s < -0.1 else "π‘"
|
| 521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
md += "\n---\n\n"
|
| 523 |
return md
|
| 524 |
|
|
|
|
| 50 |
from agents.modality_detector import detect_modality
|
| 51 |
modality = detect_modality(img)
|
| 52 |
adj = modality.score_adjustments
|
| 53 |
+
|
| 54 |
+
print(f"[FORENSIQ] Modality: {modality.modality} (conf={modality.confidence})", file=sys.stderr)
|
| 55 |
+
print(f"[FORENSIQ] Adjustments: {len(adj)} tests recalibrated", file=sys.stderr)
|
| 56 |
|
| 57 |
# Signal processing agents (fast, run in parallel with modality adjustments)
|
| 58 |
signal_agents = [
|
|
|
|
| 81 |
try:
|
| 82 |
results[name] = future.result()
|
| 83 |
except Exception as e:
|
| 84 |
+
print(f"[FORENSIQ] Agent '{name}' FAILED: {e}", file=sys.stderr)
|
| 85 |
results[name] = AgentEvidence(
|
| 86 |
agent_name=f"{name.title()} Agent (Error)",
|
| 87 |
violation_score=0.0,
|
|
|
|
| 101 |
results.get("text"),
|
| 102 |
]
|
| 103 |
ordered = [r for r in ordered if r is not None]
|
| 104 |
+
|
| 105 |
+
# Log agent scores for debugging
|
| 106 |
+
for a in ordered:
|
| 107 |
+
status = "ACTIVE" if a.failure_prob < 0.8 else "FAILED"
|
| 108 |
+
print(f"[FORENSIQ] {a.agent_name}: score={a.violation_score:+.3f} conf={a.confidence:.3f} fail={a.failure_prob:.2f} [{status}]", file=sys.stderr)
|
| 109 |
+
|
| 110 |
+
# FIX Bug 3: Filter out failed agents BEFORE Bayesian synthesis
|
| 111 |
+
# Failed agents (failure_prob >= 0.8) contribute no evidence β they're
|
| 112 |
+
# ghost entries that shouldn't appear in the reasoning tree or affect
|
| 113 |
+
# the active agent count display.
|
| 114 |
+
active_agents = [r for r in ordered if r.failure_prob < 0.8]
|
| 115 |
+
failed_agents = [r for r in ordered if r.failure_prob >= 0.8]
|
| 116 |
+
|
| 117 |
+
n_active = len(active_agents)
|
| 118 |
+
n_total = len(ordered)
|
| 119 |
+
print(f"[FORENSIQ] Active agents: {n_active}/{n_total}", file=sys.stderr)
|
| 120 |
+
|
| 121 |
+
# Bayesian synthesis β only pass active agents
|
| 122 |
+
verdict = bayesian_synthesis(active_agents)
|
| 123 |
|
| 124 |
# Attach modality info to verdict for reporting β include ALL indicators for diagnostics
|
| 125 |
verdict.reasoning_tree["modality"] = {
|
|
|
|
| 129 |
if not isinstance(v, np.ndarray) and k != "modality_scores"},
|
| 130 |
"modality_scores": modality.indicators.get("modality_scores", {}),
|
| 131 |
"adjustments_applied": len(modality.score_adjustments),
|
| 132 |
+
"adjustments_list": list(modality.score_adjustments.keys())[:10],
|
| 133 |
}
|
| 134 |
+
|
| 135 |
+
# Track failed agents in the tree for transparency
|
| 136 |
+
if failed_agents:
|
| 137 |
+
verdict.reasoning_tree["failed_agents"] = [
|
| 138 |
+
{"name": a.agent_name, "reason": a.rationale[:200]}
|
| 139 |
+
for a in failed_agents
|
| 140 |
+
]
|
| 141 |
|
| 142 |
# Generate explanations
|
| 143 |
verdict.forensic_report = generate_forensic_report(verdict)
|
| 144 |
reasoning = generate_reasoning_tree(verdict)
|
| 145 |
verdict.court_brief = generate_court_brief(verdict)
|
| 146 |
|
| 147 |
+
# Return ALL agents (including failed) for display, but verdict uses only active
|
| 148 |
return ordered, verdict, reasoning
|
| 149 |
|
| 150 |
|
|
|
|
| 202 |
for agent in agent_results:
|
| 203 |
short_name = agent.agent_name.replace(" Agent", "").replace(" Characteristics", "")
|
| 204 |
names.append(short_name)
|
|
|
|
| 205 |
display_score = (agent.violation_score + 1) * 50
|
| 206 |
scores.append(display_score)
|
| 207 |
if agent.violation_score > 0.2:
|
|
|
|
| 211 |
else:
|
| 212 |
colors.append("gold")
|
| 213 |
|
|
|
|
| 214 |
names_closed = names + [names[0]]
|
| 215 |
scores_closed = scores + [scores[0]]
|
| 216 |
|
|
|
|
| 225 |
name="Violation Score",
|
| 226 |
))
|
| 227 |
|
|
|
|
| 228 |
fig.add_trace(go.Scatterpolar(
|
| 229 |
r=[50] * (len(names) + 1),
|
| 230 |
theta=names_closed,
|
|
|
|
| 322 |
yaxis=dict(showticklabels=False, scaleanchor="x"),
|
| 323 |
)
|
| 324 |
return fig
|
|
|
|
| 325 |
fig = go.Figure()
|
| 326 |
fig.update_layout(height=400, title="FFT Spectrum (not available)")
|
| 327 |
return fig
|
|
|
|
| 455 |
mod_name = mod_info.get("detected", "UNKNOWN")
|
| 456 |
mod_conf = mod_info.get("confidence", 0)
|
| 457 |
mod_adj = mod_info.get("adjustments_applied", 0)
|
| 458 |
+
mod_label = {
|
| 459 |
+
"PORTRAIT_MODE": "π± Portrait Mode",
|
| 460 |
+
"MESSAGING": "π¬ Messaging App",
|
| 461 |
+
"SMARTPHONE": "π± Smartphone",
|
| 462 |
+
"SCREENSHOT": "π₯οΈ Screenshot",
|
| 463 |
+
"DSLR": "π· DSLR",
|
| 464 |
+
"MACRO_DSLR": "π¬ Macro DSLR",
|
| 465 |
+
"UNKNOWN": "β Unknown",
|
| 466 |
+
}.get(mod_name, f"π· {mod_name}")
|
| 467 |
+
|
| 468 |
+
# Count active agents from verdict (which only has active agents now)
|
| 469 |
+
n_active = len(verdict.agent_results)
|
| 470 |
|
| 471 |
verdict_html = f"""
|
| 472 |
<div style="background:{bg}; color:white; padding:24px; border-radius:16px;
|
|
|
|
| 475 |
<div style="font-size:28px; font-weight:700; margin-bottom:4px;">{verdict.verdict}</div>
|
| 476 |
<div style="font-size:42px; font-weight:800;">{prob:.1%}</div>
|
| 477 |
<div style="font-size:14px; opacity:0.9; margin-top:4px;">
|
| 478 |
+
Confidence: {verdict.confidence} | Agents: {n_active}/7 active
|
| 479 |
</div>
|
| 480 |
<div style="font-size:12px; opacity:0.7; margin-top:6px; border-top:1px solid rgba(255,255,255,0.2); padding-top:6px;">
|
| 481 |
Modality: {mod_label} ({mod_conf:.0%}) | {mod_adj} test(s) recalibrated
|
|
|
|
| 509 |
_build_agent_details_md(agent_results),
|
| 510 |
)
|
| 511 |
except Exception as e:
|
| 512 |
+
import traceback
|
| 513 |
+
traceback.print_exc(file=sys.stderr)
|
| 514 |
error_html = f"""
|
| 515 |
<div style="background:linear-gradient(135deg, #6c757d, #495057); color:white;
|
| 516 |
padding:24px; border-radius:16px; text-align:center;">
|
|
|
|
| 553 |
note = sf.get("note", "")
|
| 554 |
s = sf.get("score", 0)
|
| 555 |
ic = "π΄" if s > 0.2 else "π’" if s < -0.1 else "π‘"
|
| 556 |
+
# Show modality adjustment info
|
| 557 |
+
if sf.get("modality_adjusted"):
|
| 558 |
+
adj_info = f" [Γ{sf.get('adjustment_multiplier', '?')} modality]"
|
| 559 |
+
else:
|
| 560 |
+
adj_info = ""
|
| 561 |
+
md += f"- {ic} **{test}** ({s:+.2f}{adj_info}): {note}\n"
|
| 562 |
md += "\n---\n\n"
|
| 563 |
return md
|
| 564 |
|