ketannnn commited on
Commit
4b9553f
·
1 Parent(s): 7c30c0e

fix: growth_velocity always 60% bug - handle JSON string and empty work_exp

Browse files
backend/src/ml/feature_builder.py CHANGED
@@ -55,17 +55,29 @@ def _extract_seniority(title: str) -> int:
55
 
56
 
57
  def compute_growth_velocity(work_experience: list[dict], is_funded: bool = False) -> float:
58
- if not work_experience or len(work_experience) < 2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  base = 0.6 if is_funded else 0.5
60
  return base
61
 
62
- entries = sorted(work_experience, key=lambda x: x.get("start_date", "") or "")
63
  seniority_levels = []
64
  total_months = 0.0
65
 
66
  for entry in entries:
67
- if not isinstance(entry, dict):
68
- continue
69
  title = entry.get("title") or entry.get("role") or ""
70
  seniority_levels.append(_extract_seniority(title))
71
  total_months += _parse_duration_months(entry)
 
55
 
56
 
57
  def compute_growth_velocity(work_experience: list[dict], is_funded: bool = False) -> float:
58
+ import json as _json
59
+
60
+ # Handle case where work_experience arrives as a JSON string (not yet parsed)
61
+ if isinstance(work_experience, str):
62
+ try:
63
+ work_experience = _json.loads(work_experience)
64
+ except Exception:
65
+ work_experience = []
66
+
67
+ # Filter to only valid dict entries that have a title/role
68
+ valid_entries = [e for e in (work_experience or []) if isinstance(e, dict) and (e.get("title") or e.get("role"))]
69
+
70
+ if len(valid_entries) < 2:
71
+ # Fallback: compute from YOE-like numeric if available,
72
+ # otherwise use funded signal
73
  base = 0.6 if is_funded else 0.5
74
  return base
75
 
76
+ entries = sorted(valid_entries, key=lambda x: x.get("start_date", "") or "")
77
  seniority_levels = []
78
  total_months = 0.0
79
 
80
  for entry in entries:
 
 
81
  title = entry.get("title") or entry.get("role") or ""
82
  seniority_levels.append(_extract_seniority(title))
83
  total_months += _parse_duration_months(entry)
frontend/src/app/pipeline/page.tsx CHANGED
@@ -38,6 +38,8 @@ export default function PipelinePage() {
38
  const [sessionName, setSessionName] = useState("");
39
  const [jds, setJds] = useState<JDInput[]>([{ title: "", desc: "" }]);
40
  const [file, setFile] = useState<File | null>(null);
 
 
41
 
42
  // Architecture state
43
  const [state, setState] = useState<PipelineState>(DEFAULT_STATE);
@@ -178,7 +180,7 @@ export default function PipelinePage() {
178
 
179
  for (const jdId of pendingJds) {
180
  try {
181
- await api.triggerMatch(jdId, sessionId);
182
  } catch (e: any) {
183
  if (e.message === "202_ACCEPTED") {
184
  stillPending.push(jdId);
@@ -318,6 +320,37 @@ export default function PipelinePage() {
318
  </div>
319
  </div>
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  <button onClick={startPipeline}
322
  className="w-full py-4 rounded-xl bg-[var(--color-brand)] text-white font-bold tracking-wide shadow-lg shadow-[var(--color-brand-glow)] hover:brightness-110 transition-all active:scale-[0.98]">
323
  START AUTOMATED PIPELINE
 
38
  const [sessionName, setSessionName] = useState("");
39
  const [jds, setJds] = useState<JDInput[]>([{ title: "", desc: "" }]);
40
  const [file, setFile] = useState<File | null>(null);
41
+ const [stage1TopK, setStage1TopK] = useState(100);
42
+ const [stage2TopK, setStage2TopK] = useState(40);
43
 
44
  // Architecture state
45
  const [state, setState] = useState<PipelineState>(DEFAULT_STATE);
 
180
 
181
  for (const jdId of pendingJds) {
182
  try {
183
+ await api.triggerMatch(jdId, sessionId, stage1TopK, stage2TopK);
184
  } catch (e: any) {
185
  if (e.message === "202_ACCEPTED") {
186
  stillPending.push(jdId);
 
320
  </div>
321
  </div>
322
 
323
+ <div className="mb-6 border-t border-[var(--color-border-strong)] pt-6">
324
+ <div className="text-xs font-bold text-[var(--color-muted)] mb-4 uppercase tracking-wider">⚡ Neural Reranking Depth</div>
325
+ <div className="grid grid-cols-2 gap-4">
326
+ <div className="bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl p-4">
327
+ <div className="flex justify-between text-xs mb-2">
328
+ <span className="text-[var(--color-muted)] font-medium">Stage 1 — Vector Retrieval</span>
329
+ <span className="font-bold text-[var(--color-brand-light)]">{stage1TopK}</span>
330
+ </div>
331
+ <input type="range" min={10} max={500} step={10} value={stage1TopK}
332
+ className="w-full cursor-pointer"
333
+ onChange={e => { const v = parseInt(e.target.value); setStage1TopK(v); if (stage2TopK > v) setStage2TopK(v); }}
334
+ />
335
+ <div className="flex justify-between text-[9px] text-[var(--color-dimmer)] mt-1"><span>10 (fast)</span><span>500 (thorough)</span></div>
336
+ </div>
337
+ <div className="bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl p-4">
338
+ <div className="flex justify-between text-xs mb-2">
339
+ <span className="text-[var(--color-muted)] font-medium">Stage 2 — Neural Reranker</span>
340
+ <span className="font-bold text-[var(--color-brand-light)]">{Math.min(stage2TopK, stage1TopK)}</span>
341
+ </div>
342
+ <input type="range" min={5} max={Math.min(stage1TopK, 250)} step={5} value={Math.min(stage2TopK, stage1TopK)}
343
+ className="w-full cursor-pointer"
344
+ onChange={e => setStage2TopK(parseInt(e.target.value))}
345
+ />
346
+ <div className="flex justify-between text-[9px] text-[var(--color-dimmer)] mt-1"><span>5 (fast)</span><span>{Math.min(stage1TopK, 250)} (deep)</span></div>
347
+ </div>
348
+ </div>
349
+ <p className="text-[9px] text-[var(--color-dimmer)] mt-2 leading-relaxed">
350
+ Stage 2 = cross-encoder neural reranking (CPU-heavy). On Hugging Face ~40 takes ~20s. ~250 may cause timeout.
351
+ </p>
352
+ </div>
353
+
354
  <button onClick={startPipeline}
355
  className="w-full py-4 rounded-xl bg-[var(--color-brand)] text-white font-bold tracking-wide shadow-lg shadow-[var(--color-brand-glow)] hover:brightness-110 transition-all active:scale-[0.98]">
356
  START AUTOMATED PIPELINE
frontend/src/app/sessions/[id]/page.tsx CHANGED
@@ -28,8 +28,6 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
28
  const [weights, setWeights] = useState(DEFAULT_WEIGHTS);
29
  const [reranking, setReranking] = useState(false);
30
  const [loading, setLoading] = useState(true);
31
- const [stage1TopK, setStage1TopK] = useState(100); // how many to retrieve from vector DB
32
- const [stage2TopK, setStage2TopK] = useState(40); // how many to pass to neural reranker
33
 
34
  const debounce = useCallback(<T extends unknown[]>(fn: (...args: T) => void, ms: number) => {
35
  let t: ReturnType<typeof setTimeout>;
@@ -106,7 +104,7 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
106
 
107
  const poll = async () => {
108
  try {
109
- const r = await api.triggerMatch(selectedJD, sessionId, stage1TopK, stage2TopK);
110
  setMatch(r);
111
  setBaseResults(r.results);
112
  setMatching(false);
@@ -233,41 +231,12 @@ export default function SessionDetailPage({ params }: { params: Promise<{ id: st
233
  <p className="text-sm text-[var(--color-muted)] mt-1">Sifting through vector embeddings & generating insights.</p>
234
  </div>
235
  ) : !match ? (
236
- <div className="flex flex-col items-center justify-center h-full text-center py-20 animate-fade-in">
237
  <div className="text-5xl mb-4">🎯</div>
238
  <h3 className="text-xl font-bold mb-2">No Match Matrix Found</h3>
239
  <p className="text-sm text-[var(--color-muted)] mb-6 max-w-sm">
240
  You haven't run the candidates against "{currentJdObj?.title}" yet.
241
  </p>
242
- {/* Pipeline Depth Controls */}
243
- <div className="bg-[var(--color-surface-2)] border border-[var(--color-border-strong)] rounded-xl p-5 w-full max-w-sm mb-6 text-left space-y-4">
244
- <div className="text-[10px] uppercase font-bold text-[var(--color-muted)] tracking-wider">Pipeline Depth Settings</div>
245
- <div>
246
- <div className="flex justify-between text-xs mb-1">
247
- <span className="text-[var(--color-muted)]">Stage 1 — Vector Retrieval</span>
248
- <span className="font-bold text-[var(--color-brand-light)]">{stage1TopK} candidates</span>
249
- </div>
250
- <input type="range" min={10} max={500} step={10} value={stage1TopK}
251
- className="w-full cursor-pointer"
252
- onChange={e => { const v = parseInt(e.target.value); setStage1TopK(v); if (stage2TopK > v) setStage2TopK(v); }}
253
- />
254
- <div className="flex justify-between text-[9px] text-[var(--color-dimmer)] mt-0.5"><span>10 (fastest)</span><span>500 (most thorough)</span></div>
255
- </div>
256
- <div>
257
- <div className="flex justify-between text-xs mb-1">
258
- <span className="text-[var(--color-muted)]">Stage 2 — Neural Reranker</span>
259
- <span className="font-bold text-[var(--color-brand-light)]">{Math.min(stage2TopK, stage1TopK)} candidates</span>
260
- </div>
261
- <input type="range" min={5} max={Math.min(stage1TopK, 250)} step={5} value={Math.min(stage2TopK, stage1TopK)}
262
- className="w-full cursor-pointer"
263
- onChange={e => setStage2TopK(parseInt(e.target.value))}
264
- />
265
- <div className="flex justify-between text-[9px] text-[var(--color-dimmer)] mt-0.5"><span>5 (fast)</span><span>{Math.min(stage1TopK, 250)} (deep)</span></div>
266
- </div>
267
- <p className="text-[9px] text-[var(--color-dimmer)] leading-relaxed">
268
- ⚡ Higher Stage 2 = better accuracy but slower. On Hugging Face CPU, ~40 takes ~20s. ~250 may timeout.
269
- </p>
270
- </div>
271
  <button onClick={handleMatch} className="px-6 py-3 rounded-xl bg-[var(--color-brand)] text-white font-bold tracking-wide shadow-lg hover:brightness-110 active:scale-95 transition-all">
272
  ⚡ Execute Match Pipeline
273
  </button>
 
28
  const [weights, setWeights] = useState(DEFAULT_WEIGHTS);
29
  const [reranking, setReranking] = useState(false);
30
  const [loading, setLoading] = useState(true);
 
 
31
 
32
  const debounce = useCallback(<T extends unknown[]>(fn: (...args: T) => void, ms: number) => {
33
  let t: ReturnType<typeof setTimeout>;
 
104
 
105
  const poll = async () => {
106
  try {
107
+ const r = await api.triggerMatch(selectedJD, sessionId);
108
  setMatch(r);
109
  setBaseResults(r.results);
110
  setMatching(false);
 
231
  <p className="text-sm text-[var(--color-muted)] mt-1">Sifting through vector embeddings & generating insights.</p>
232
  </div>
233
  ) : !match ? (
234
+ <div className="flex flex-col items-center justify-center h-full text-center py-32 animate-fade-in">
235
  <div className="text-5xl mb-4">🎯</div>
236
  <h3 className="text-xl font-bold mb-2">No Match Matrix Found</h3>
237
  <p className="text-sm text-[var(--color-muted)] mb-6 max-w-sm">
238
  You haven't run the candidates against "{currentJdObj?.title}" yet.
239
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  <button onClick={handleMatch} className="px-6 py-3 rounded-xl bg-[var(--color-brand)] text-white font-bold tracking-wide shadow-lg hover:brightness-110 active:scale-95 transition-all">
241
  ⚡ Execute Match Pipeline
242
  </button>