Vedant Sanjay Jadhav commited on
Commit
bf39fe7
·
1 Parent(s): 5af7440

fix: update Pydantic ExplanationInput defaults, rename assistant to RAZOR, and add null-safety to scenario distribution

Browse files
frontend/app/scenarios/page.tsx CHANGED
@@ -172,50 +172,56 @@ export default function ScenariosPage() {
172
  {/* Execution Results */}
173
  {executionState === "COMPLETE" && result && (
174
  <div className="space-y-6">
175
- <div className="bg-brand-black border border-brand-border p-6 space-y-6">
176
- <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between border-b border-brand-border/60 pb-4 gap-4">
177
- <div>
178
- <span className="font-mono text-xs text-brand-red uppercase font-bold block mb-1">
179
- REPLAY EXECUTION COMPLETE
180
- </span>
181
- <h3 className="font-sans font-bold text-2xl text-white">
182
- {result.scenario_name} ({result.merchant_id})
183
- </h3>
184
- </div>
185
-
186
- <div className={`inline-flex items-center space-x-2 border px-4 py-2 font-mono text-xs uppercase font-bold ${
187
- isAlert
188
- ? "bg-brand-red/10 border-brand-red/40 text-brand-red"
189
- : isVerify
190
- ? "bg-amber-400/10 border-amber-400/40 text-amber-400"
191
- : "bg-emerald-400/10 border-emerald-400/40 text-emerald-400"
192
- }`}>
193
- <Icon className="w-4 h-4 stroke-[2.5]" />
194
- <span>FINAL INCIDENT STATE: {result.final_incident_state}</span>
195
- </div>
196
  </div>
 
 
 
 
 
 
 
 
 
 
 
197
 
198
- <div className="grid grid-cols-1 sm:grid-cols-4 gap-4 font-mono text-xs">
199
- <div className="bg-brand-dark border border-brand-border/60 p-4">
200
- <span className="text-brand-muted block mb-1">Total Transactions:</span>
201
- <span className="text-white text-xl font-bold">{result.total_transactions}</span>
 
 
 
 
 
 
202
  </div>
203
 
204
- <div className="bg-brand-dark border border-brand-border/60 p-4">
205
- <span className="text-brand-muted block mb-1">Engine Processing Time:</span>
206
- <span className="text-white text-xl font-bold">{formatFixed(result.replay_time_ms, 1)} ms</span>
207
- </div>
 
208
 
209
- <div className="bg-brand-dark border border-brand-border/60 p-4">
210
- <span className="text-brand-muted block mb-1">Normal Windows:</span>
211
- <span className="text-emerald-400 text-xl font-bold">{result.incident_state_distribution.NORMAL}</span>
212
- </div>
213
 
214
- <div className="bg-brand-dark border border-brand-border/60 p-4">
215
- <span className="text-brand-muted block mb-1">Alert Incident Windows:</span>
216
- <span className="text-brand-red text-xl font-bold">{result.incident_state_distribution.ALERT}</span>
 
 
 
 
 
 
217
  </div>
218
- </div>
219
 
220
  {/* Data Provenance Footer */}
221
  {meta && (
 
172
  {/* Execution Results */}
173
  {executionState === "COMPLETE" && result && (
174
  <div className="space-y-6">
175
+ {result.error ? (
176
+ <div className="bg-brand-red/10 border border-brand-red/40 p-6 font-mono text-xs text-brand-red space-y-2">
177
+ <span className="font-bold uppercase block">SCENARIO REPLAY ERROR</span>
178
+ <p>{result.error}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  </div>
180
+ ) : (
181
+ <div className="bg-brand-black border border-brand-border p-6 space-y-6">
182
+ <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between border-b border-brand-border/60 pb-4 gap-4">
183
+ <div>
184
+ <span className="font-mono text-xs text-brand-red uppercase font-bold block mb-1">
185
+ REPLAY EXECUTION COMPLETE
186
+ </span>
187
+ <h3 className="font-sans font-bold text-2xl text-white">
188
+ {result.scenario_name} ({result.merchant_id || "M_DEMO"})
189
+ </h3>
190
+ </div>
191
 
192
+ <div className={`inline-flex items-center space-x-2 border px-4 py-2 font-mono text-xs uppercase font-bold ${
193
+ isAlert
194
+ ? "bg-brand-red/10 border-brand-red/40 text-brand-red"
195
+ : isVerify
196
+ ? "bg-amber-400/10 border-amber-400/40 text-amber-400"
197
+ : "bg-emerald-400/10 border-emerald-400/40 text-emerald-400"
198
+ }`}>
199
+ <Icon className="w-4 h-4 stroke-[2.5]" />
200
+ <span>FINAL INCIDENT STATE: {result.final_incident_state || "NORMAL"}</span>
201
+ </div>
202
  </div>
203
 
204
+ <div className="grid grid-cols-1 sm:grid-cols-4 gap-4 font-mono text-xs">
205
+ <div className="bg-brand-dark border border-brand-border/60 p-4">
206
+ <span className="text-brand-muted block mb-1">Total Transactions:</span>
207
+ <span className="text-white text-xl font-bold">{result.total_transactions ?? 0}</span>
208
+ </div>
209
 
210
+ <div className="bg-brand-dark border border-brand-border/60 p-4">
211
+ <span className="text-brand-muted block mb-1">Engine Processing Time:</span>
212
+ <span className="text-white text-xl font-bold">{formatFixed(result.replay_time_ms ?? 0, 1)} ms</span>
213
+ </div>
214
 
215
+ <div className="bg-brand-dark border border-brand-border/60 p-4">
216
+ <span className="text-brand-muted block mb-1">Normal Windows:</span>
217
+ <span className="text-emerald-400 text-xl font-bold">{result.incident_state_distribution?.NORMAL ?? 0}</span>
218
+ </div>
219
+
220
+ <div className="bg-brand-dark border border-brand-border/60 p-4">
221
+ <span className="text-brand-muted block mb-1">Alert Incident Windows:</span>
222
+ <span className="text-brand-red text-xl font-bold">{result.incident_state_distribution?.ALERT ?? 0}</span>
223
+ </div>
224
  </div>
 
225
 
226
  {/* Data Provenance Footer */}
227
  {meta && (
frontend/components/dashboard/SLMChatDrawer.tsx CHANGED
@@ -1,7 +1,7 @@
1
  "use client";
2
 
3
  import React, { useState } from "react";
4
- import { MessageSquare, Send, X, Bot, Loader2, Sparkles, ShieldCheck } from "lucide-react";
5
  import { AnalyzeTransactionResponse } from "@/lib/types";
6
  import { explainEvidencePayload } from "@/lib/api";
7
 
@@ -25,7 +25,7 @@ export default function SLMChatDrawer({ data }: Props) {
25
  {
26
  id: "init",
27
  sender: "slm",
28
- text: "Hello, I am the RazorShield Qwen2.5-0.5B SLM Assistant. Ask me anything about the current transaction risk assessment, merchant evidence, or policy decision.",
29
  timestamp: new Date().toLocaleTimeString(),
30
  },
31
  ]);
@@ -55,25 +55,33 @@ export default function SLMChatDrawer({ data }: Props) {
55
  const start = performance.now();
56
 
57
  try {
58
- // Build structured evidence object from active data
59
- const evidenceContext = data
60
- ? {
61
- merchant_id: data.merchant_id,
62
- transaction_id: data.transaction_id,
63
- decision: data.decision.action,
64
- policy_mode: data.decision.policy_mode,
65
- fraud_probability: data.transaction_risk.fraud_probability,
66
- spike_probability: data.merchant_risk.spike_probability,
67
- fraud_excess_ratio: data.merchant_risk.fraud_excess_ratio,
68
- velocity_ratio: data.merchant_risk.velocity_ratio,
69
- incident_state: data.merchant_risk.incident_state,
70
- campaign_active: data.campaign.active,
71
- user_question: textToSend,
72
- }
73
- : {
74
- user_question: textToSend,
75
- note: "No active transaction selected yet.",
76
- };
 
 
 
 
 
 
 
 
77
 
78
  const res = await explainEvidencePayload(JSON.stringify(evidenceContext));
79
  const latency = Math.round(performance.now() - start);
@@ -96,7 +104,7 @@ export default function SLMChatDrawer({ data }: Props) {
96
  const errorMsg: ChatMessage = {
97
  id: Math.random().toString(),
98
  sender: "slm",
99
- text: `Error reaching SLM assistant: ${err.message || "Request timed out"}`,
100
  timestamp: new Date().toLocaleTimeString(),
101
  };
102
  setMessages((prev) => [...prev, errorMsg]);
@@ -110,10 +118,10 @@ export default function SLMChatDrawer({ data }: Props) {
110
  {/* Floating Co-Pilot Toggle Button */}
111
  <button
112
  onClick={() => setIsOpen(!isOpen)}
113
- className="fixed bottom-6 right-6 bg-brand-red hover:bg-brand-red-hover text-white px-4 py-3 font-mono text-xs uppercase font-bold flex items-center space-x-2 shadow-2xl z-50 transition-transform active:scale-95"
114
  >
115
  <Bot className="w-4 h-4" />
116
- <span>SLM CO-PILOT (QWEN2.5-0.5B)</span>
117
  <Sparkles className="w-3.5 h-3.5 fill-white text-white animate-pulse" />
118
  </button>
119
 
@@ -124,7 +132,7 @@ export default function SLMChatDrawer({ data }: Props) {
124
  <div className="p-4 border-b border-brand-border/60 bg-brand-dark flex items-center justify-between font-mono text-xs">
125
  <div className="flex items-center space-x-2 text-brand-red font-bold uppercase">
126
  <Bot className="w-4 h-4" />
127
- <span>SLM RISK ASSISTANT</span>
128
  </div>
129
 
130
  <div className="flex items-center space-x-3 text-brand-muted">
@@ -145,7 +153,7 @@ export default function SLMChatDrawer({ data }: Props) {
145
  <span>
146
  CONTEXT:{" "}
147
  <strong className="text-white">
148
- {data ? `${data.merchant_id} / ${data.transaction_id}` : "NO TRANSACTION SELECTED"}
149
  </strong>
150
  </span>
151
  {data && (
@@ -165,7 +173,7 @@ export default function SLMChatDrawer({ data }: Props) {
165
  }`}
166
  >
167
  <div className="flex items-center space-x-2 text-[10px] text-brand-muted">
168
- <span>{msg.sender === "user" ? "ANALYST" : "SLM ASSISTANT"}</span>
169
  <span>•</span>
170
  <span>{msg.timestamp}</span>
171
  {msg.latencyMs && (
@@ -188,7 +196,7 @@ export default function SLMChatDrawer({ data }: Props) {
188
  {isLoading && (
189
  <div className="flex items-center space-x-2 font-mono text-[11px] text-brand-red animate-pulse p-2">
190
  <Loader2 className="w-3.5 h-3.5 animate-spin" />
191
- <span>Qwen2.5-0.5B evaluating structured evidence...</span>
192
  </div>
193
  )}
194
  </div>
@@ -218,13 +226,13 @@ export default function SLMChatDrawer({ data }: Props) {
218
  value={inputQuery}
219
  onChange={(e) => setInputQuery(e.target.value)}
220
  onKeyDown={(e) => e.key === "Enter" && handleSend()}
221
- placeholder="Ask SLM assistant about risk context..."
222
  className="flex-1 bg-brand-black border border-brand-border text-white px-3 py-2 font-mono text-xs focus:outline-none focus:border-brand-red"
223
  />
224
  <button
225
  onClick={() => handleSend()}
226
  disabled={isLoading || !inputQuery.trim()}
227
- className="bg-brand-red hover:bg-brand-red-hover disabled:bg-brand-border text-white px-3 py-2 transition-colors"
228
  >
229
  <Send className="w-4 h-4" />
230
  </button>
 
1
  "use client";
2
 
3
  import React, { useState } from "react";
4
+ import { MessageSquare, Send, X, Bot, Loader2, Sparkles } from "lucide-react";
5
  import { AnalyzeTransactionResponse } from "@/lib/types";
6
  import { explainEvidencePayload } from "@/lib/api";
7
 
 
25
  {
26
  id: "init",
27
  sender: "slm",
28
+ text: "Hello! I am RAZOR, your RazorShield AI Risk Assistant powered by Qwen2.5-0.5B. Ask me anything about the active transaction evaluation, merchant evidence signals, or policy decision.",
29
  timestamp: new Date().toLocaleTimeString(),
30
  },
31
  ]);
 
55
  const start = performance.now();
56
 
57
  try {
58
+ // Build complete, valid EvidenceInput object for Pydantic schema
59
+ const evidenceContext = {
60
+ merchant_id: data?.merchant_id || "M_101",
61
+ incident_state: data?.merchant_risk?.incident_state || "NORMAL",
62
+ severity: data?.merchant_risk?.severity || "LOW",
63
+ incident_score: data?.merchant_risk?.incident_score ?? 0.05,
64
+ spike_probability: data?.merchant_risk?.spike_probability ?? 0.0,
65
+ fraud_excess_ratio: data?.merchant_risk?.fraud_excess_ratio ?? 1.0,
66
+ velocity_ratio: data?.merchant_risk?.velocity_ratio ?? 1.0,
67
+ suspicious_windows: data?.merchant_risk?.suspicious_windows ?? 0,
68
+ total_suspicious_windows: data?.merchant_risk?.suspicious_windows ?? 0,
69
+ campaign_active: Boolean(data?.campaign?.active),
70
+ policy_mode: data?.decision?.policy_mode || "BALANCED",
71
+ recommended_action: data?.decision?.action || "APPROVE",
72
+ signals: [
73
+ {
74
+ name: "fraud_excess_ratio",
75
+ value: data?.merchant_risk?.fraud_excess_ratio ?? 1.0,
76
+ direction: (data?.merchant_risk?.fraud_excess_ratio ?? 1.0) > 1.5 ? "elevated" : "normal",
77
+ },
78
+ {
79
+ name: "velocity_ratio",
80
+ value: data?.merchant_risk?.velocity_ratio ?? 1.0,
81
+ direction: (data?.merchant_risk?.velocity_ratio ?? 1.0) > 2.0 ? "elevated" : "normal",
82
+ },
83
+ ],
84
+ };
85
 
86
  const res = await explainEvidencePayload(JSON.stringify(evidenceContext));
87
  const latency = Math.round(performance.now() - start);
 
104
  const errorMsg: ChatMessage = {
105
  id: Math.random().toString(),
106
  sender: "slm",
107
+ text: `Error reaching RAZOR assistant: ${err.message || "Request failed"}`,
108
  timestamp: new Date().toLocaleTimeString(),
109
  };
110
  setMessages((prev) => [...prev, errorMsg]);
 
118
  {/* Floating Co-Pilot Toggle Button */}
119
  <button
120
  onClick={() => setIsOpen(!isOpen)}
121
+ className="fixed bottom-6 right-6 bg-brand-red hover:bg-brand-red-hover text-white px-4 py-3 font-mono text-xs uppercase font-bold flex items-center space-x-2 shadow-2xl z-50 transition-transform active:scale-95 rounded-none"
122
  >
123
  <Bot className="w-4 h-4" />
124
+ <span>RAZOR AI ASSISTANT</span>
125
  <Sparkles className="w-3.5 h-3.5 fill-white text-white animate-pulse" />
126
  </button>
127
 
 
132
  <div className="p-4 border-b border-brand-border/60 bg-brand-dark flex items-center justify-between font-mono text-xs">
133
  <div className="flex items-center space-x-2 text-brand-red font-bold uppercase">
134
  <Bot className="w-4 h-4" />
135
+ <span>RAZOR AI ASSISTANT</span>
136
  </div>
137
 
138
  <div className="flex items-center space-x-3 text-brand-muted">
 
153
  <span>
154
  CONTEXT:{" "}
155
  <strong className="text-white">
156
+ {data ? `${data.merchant_id} / ${data.transaction_id}` : "DEFAULT CONTEXT (M_101)"}
157
  </strong>
158
  </span>
159
  {data && (
 
173
  }`}
174
  >
175
  <div className="flex items-center space-x-2 text-[10px] text-brand-muted">
176
+ <span>{msg.sender === "user" ? "ANALYST" : "RAZOR ASSISTANT"}</span>
177
  <span>•</span>
178
  <span>{msg.timestamp}</span>
179
  {msg.latencyMs && (
 
196
  {isLoading && (
197
  <div className="flex items-center space-x-2 font-mono text-[11px] text-brand-red animate-pulse p-2">
198
  <Loader2 className="w-3.5 h-3.5 animate-spin" />
199
+ <span>RAZOR assistant evaluating structured evidence...</span>
200
  </div>
201
  )}
202
  </div>
 
226
  value={inputQuery}
227
  onChange={(e) => setInputQuery(e.target.value)}
228
  onKeyDown={(e) => e.key === "Enter" && handleSend()}
229
+ placeholder="Ask RAZOR assistant about risk context..."
230
  className="flex-1 bg-brand-black border border-brand-border text-white px-3 py-2 font-mono text-xs focus:outline-none focus:border-brand-red"
231
  />
232
  <button
233
  onClick={() => handleSend()}
234
  disabled={isLoading || !inputQuery.trim()}
235
+ className="bg-brand-red hover:bg-brand-red-hover disabled:bg-brand-border text-white px-3 py-2 transition-colors rounded-none"
236
  >
237
  <Send className="w-4 h-4" />
238
  </button>
src/explanation/schemas.py CHANGED
@@ -13,14 +13,14 @@ from pydantic import BaseModel, Field
13
  class ExplanationInput(BaseModel):
14
  """Input structured evidence generated by RazorShield Risk/Incident Engines."""
15
 
16
- merchant_id: str = Field(..., description="Merchant ID")
17
- incident_state: Literal["NORMAL", "INVESTIGATE", "ALERT"] = Field(..., description="Incident state")
18
- severity: Literal["LOW", "MEDIUM", "HIGH"] = Field(..., description="Severity level")
19
- incident_score: float = Field(..., ge=0.0, le=1.0, description="Policy incident score")
20
- spike_probability: float = Field(..., ge=0.0, le=1.0, description="Spike probability")
21
- fraud_excess_ratio: float = Field(..., ge=0.0, description="Fraud excess ratio")
22
- velocity_ratio: float = Field(..., ge=0.0, description="Velocity ratio")
23
- suspicious_windows: int = Field(..., ge=0, description="Consecutive suspicious windows")
24
  total_suspicious_windows: int = Field(default=0, ge=0, description="Total suspicious windows")
25
  campaign_active: bool = Field(default=False, description="Whether campaign is active")
26
  policy_mode: str = Field(default="BALANCED", description="Policy operating mode")
 
13
  class ExplanationInput(BaseModel):
14
  """Input structured evidence generated by RazorShield Risk/Incident Engines."""
15
 
16
+ merchant_id: str = Field(default="M_101", description="Merchant ID")
17
+ incident_state: Literal["NORMAL", "INVESTIGATE", "ALERT"] = Field(default="NORMAL", description="Incident state")
18
+ severity: Literal["LOW", "MEDIUM", "HIGH"] = Field(default="LOW", description="Severity level")
19
+ incident_score: float = Field(default=0.05, ge=0.0, le=1.0, description="Policy incident score")
20
+ spike_probability: float = Field(default=0.0, ge=0.0, le=1.0, description="Spike probability")
21
+ fraud_excess_ratio: float = Field(default=1.0, ge=0.0, description="Fraud excess ratio")
22
+ velocity_ratio: float = Field(default=1.0, ge=0.0, description="Velocity ratio")
23
+ suspicious_windows: int = Field(default=0, ge=0, description="Consecutive suspicious windows")
24
  total_suspicious_windows: int = Field(default=0, ge=0, description="Total suspicious windows")
25
  campaign_active: bool = Field(default=False, description="Whether campaign is active")
26
  policy_mode: str = Field(default="BALANCED", description="Policy operating mode")