FairValue commited on
Commit
778ab74
·
1 Parent(s): aa3c3cf

feat: upgrade NLP engine with deep search, fallbacks, and neutral signal detection

Browse files
api/main.py CHANGED
@@ -119,6 +119,7 @@ async def scout_player(player: str, club: str = "", interested_club: str = ""):
119
  "agent": nlp["agent"],
120
  "logs": nlp.get("_logs", []),
121
  "from_cache": nlp.get("_from_cache", False),
 
122
  }
123
 
124
 
@@ -189,20 +190,32 @@ def _fetch_nlp_intelligence(
189
  scores = {'durability': 0.0, 'recency': 0.0, 'agent': 0.0}
190
  logs = []
191
 
 
192
  for axis, query in axes.items():
193
  try:
194
- snippets = list(ddgs.text(query.strip(), max_results=3))
195
- sentiments = [
196
- TextBlob(r.get('body', '') + ' ' + r.get('title', '')).sentiment.polarity
197
- for r in snippets
198
- ]
199
- avg_pol = sum(sentiments) / len(sentiments) if sentiments else 0.0
200
- scores[axis] = float(avg_pol)
201
- logs.append(f"Scraped {axis}: Polarity {avg_pol:.2f}")
 
 
 
 
 
 
 
 
 
 
 
202
  except Exception as e:
203
  logs.append(f"Failed {axis}: {str(e)}")
204
 
205
- result = {**scores, '_ts': time.time(), '_logs': logs, '_from_cache': False}
206
  _nlp_cache[cache_key] = result
207
  return result
208
 
@@ -332,6 +345,7 @@ async def evaluate_player(req: PlayerEvaluateRequest):
332
  },
333
  "nlp_results": {"durability": dur, "recency": rec, "agent": agnt},
334
  "nlp_cached": nlp.get('_from_cache', False),
 
335
  "logs": logs,
336
  "shap_data": shap_data,
337
  }
 
119
  "agent": nlp["agent"],
120
  "logs": nlp.get("_logs", []),
121
  "from_cache": nlp.get("_from_cache", False),
122
+ "nlp_found": nlp.get("_found_any", False)
123
  }
124
 
125
 
 
190
  scores = {'durability': 0.0, 'recency': 0.0, 'agent': 0.0}
191
  logs = []
192
 
193
+ found_any = False
194
  for axis, query in axes.items():
195
  try:
196
+ # Increase results to 10 for better sentiment spread
197
+ snippets = list(ddgs.text(query.strip(), max_results=10))
198
+
199
+ # Fallback: if no results, try a broader search without the clubs
200
+ if not snippets:
201
+ fallback_query = f"{player_name} {axis} news"
202
+ snippets = list(ddgs.text(fallback_query, max_results=5))
203
+
204
+ if snippets:
205
+ found_any = True
206
+ sentiments = [
207
+ TextBlob(r.get('body', '') + ' ' + r.get('title', '')).sentiment.polarity
208
+ for r in snippets
209
+ ]
210
+ avg_pol = sum(sentiments) / len(sentiments) if sentiments else 0.0
211
+ scores[axis] = float(avg_pol)
212
+ logs.append(f"Scraped {axis}: Polarity {avg_pol:.2f} ({len(snippets)} results)")
213
+ else:
214
+ logs.append(f"No results for {axis} (Primary & Fallback)")
215
  except Exception as e:
216
  logs.append(f"Failed {axis}: {str(e)}")
217
 
218
+ result = {**scores, '_ts': time.time(), '_logs': logs, '_from_cache': False, '_found_any': found_any}
219
  _nlp_cache[cache_key] = result
220
  return result
221
 
 
345
  },
346
  "nlp_results": {"durability": dur, "recency": rec, "agent": agnt},
347
  "nlp_cached": nlp.get('_from_cache', False),
348
+ "nlp_found": nlp.get('_found_any', False),
349
  "logs": logs,
350
  "shap_data": shap_data,
351
  }
fairvalue-webapp/src/components/ReportTemplate.tsx CHANGED
@@ -25,6 +25,7 @@ export interface ReportProps {
25
  recency: number
26
  agent: number
27
  }
 
28
  }
29
  }
30
 
@@ -164,20 +165,20 @@ export const ReportTemplate = forwardRef<HTMLDivElement, ReportProps>(({ form, r
164
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px', background: '#f8fafc', padding: '24px', borderRadius: '12px', border: '1px solid #e2e8f0' }}>
165
  <div>
166
  <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Recent Form &amp; Impact</div>
167
- <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.recency ?? 0) === 0 ? '#94a3b8' : (result.nlp_results?.recency ?? 0) < 0 ? '#ef4444' : '#22c55e' }}>
168
- {(result.nlp_results?.recency ?? 0) === 0 ? 'No signals detected' : `${(result.nlp_results?.recency ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.recency ?? 0).toFixed(2)}`}
169
  </div>
170
  </div>
171
  <div>
172
  <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Injury / Availability</div>
173
- <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.durability ?? 0) === 0 ? '#94a3b8' : (result.nlp_results?.durability ?? 0) < 0 ? '#ef4444' : '#0f172a' }}>
174
- {(result.nlp_results?.durability ?? 0) === 0 ? 'No signals detected' : `${(result.nlp_results?.durability ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.durability ?? 0).toFixed(2)}`}
175
  </div>
176
  </div>
177
  <div>
178
  <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Transfer Speculation</div>
179
- <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.agent ?? 0) === 0 ? '#94a3b8' : (result.nlp_results?.agent ?? 0) < 0 ? '#ef4444' : '#22c55e' }}>
180
- {(result.nlp_results?.agent ?? 0) === 0 ? 'No signals detected' : `${(result.nlp_results?.agent ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.agent ?? 0).toFixed(2)}`}
181
  </div>
182
  </div>
183
  </div>
 
25
  recency: number
26
  agent: number
27
  }
28
+ nlp_found?: boolean
29
  }
30
  }
31
 
 
165
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px', background: '#f8fafc', padding: '24px', borderRadius: '12px', border: '1px solid #e2e8f0' }}>
166
  <div>
167
  <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Recent Form &amp; Impact</div>
168
+ <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.recency ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.recency ?? 0) < 0 ? '#ef4444' : '#22c55e' }}>
169
+ {(result.nlp_results?.recency ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.recency ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.recency ?? 0).toFixed(2)}`}
170
  </div>
171
  </div>
172
  <div>
173
  <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Injury / Availability</div>
174
+ <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.durability ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.durability ?? 0) < 0 ? '#ef4444' : '#0f172a' }}>
175
+ {(result.nlp_results?.durability ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.durability ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.durability ?? 0).toFixed(2)}`}
176
  </div>
177
  </div>
178
  <div>
179
  <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Transfer Speculation</div>
180
+ <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.agent ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.agent ?? 0) < 0 ? '#ef4444' : '#22c55e' }}>
181
+ {(result.nlp_results?.agent ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.agent ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.agent ?? 0).toFixed(2)}`}
182
  </div>
183
  </div>
184
  </div>
fairvalue-webapp/src/pages/Estimator.tsx CHANGED
@@ -25,6 +25,7 @@ interface EvalResult {
25
  ledger: Ledger
26
  nlp_results: { durability: number; recency: number; agent: number }
27
  nlp_cached: boolean
 
28
  logs: string[]
29
  shap_data: { feature: string; impact: number }[]
30
  }
@@ -125,16 +126,20 @@ function ShapChart({ data }: { data: { feature: string; impact: number }[] }) {
125
  }
126
 
127
  // ── NLP Sentiment Score ───────────────────────────────────────────────────────
128
- function SentimentScore({ label, value, icon }: { label: string; value: number; icon: string }) {
129
  const pct = Math.round(((value + 1) / 2) * 100)
130
- const color = value > 0.1 ? 'var(--profit-color)' : value < -0.1 ? 'var(--loss-color)' : 'var(--accent-blue)'
 
 
131
  return (
132
  <div>
133
  <div style={{ display:'flex', justifyContent:'space-between', marginBottom:6 }}>
134
  <span style={{ fontSize:'0.8rem', color:'var(--text-2)', display:'flex', gap:6, alignItems:'center' }}>
135
  {icon} {label}
136
  </span>
137
- <span style={{ fontSize:'0.8rem', fontWeight:700, color }}>{value > 0 ? '+' : ''}{value.toFixed(2)}</span>
 
 
138
  </div>
139
  <div className="sentiment-bar-track">
140
  <div className="sentiment-bar-fill" style={{ width:`${pct}%`, background:color }}/>
@@ -488,9 +493,9 @@ export default function Estimator() {
488
  {result.nlp_cached && <span className="badge badge-blue">Cached</span>}
489
  </div>
490
  <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
491
- <SentimentScore label="Injury / Availability" icon="🏥" value={result.nlp_results.durability}/>
492
- <SentimentScore label="Recent Form & Impact" icon="📈" value={result.nlp_results.recency}/>
493
- <SentimentScore label="Transfer Speculation" icon="🗞️" value={result.nlp_results.agent}/>
494
  </div>
495
  <div style={{ marginTop:16 }}>
496
  <button className="btn btn-ghost" style={{ fontSize:'0.78rem', padding:'6px 14px' }}
 
25
  ledger: Ledger
26
  nlp_results: { durability: number; recency: number; agent: number }
27
  nlp_cached: boolean
28
+ nlp_found?: boolean
29
  logs: string[]
30
  shap_data: { feature: string; impact: number }[]
31
  }
 
126
  }
127
 
128
  // ── NLP Sentiment Score ───────────────────────────────────────────────────────
129
+ function SentimentScore({ label, value, icon, found }: { label: string; value: number; icon: string, found?: boolean }) {
130
  const pct = Math.round(((value + 1) / 2) * 100)
131
+ const isNeutral = value === 0
132
+ const color = value > 0.1 ? 'var(--profit-color)' : value < -0.1 ? 'var(--loss-color)' : (isNeutral && found ? 'var(--text-3)' : 'var(--accent-blue)')
133
+
134
  return (
135
  <div>
136
  <div style={{ display:'flex', justifyContent:'space-between', marginBottom:6 }}>
137
  <span style={{ fontSize:'0.8rem', color:'var(--text-2)', display:'flex', gap:6, alignItems:'center' }}>
138
  {icon} {label}
139
  </span>
140
+ <span style={{ fontSize:'0.8rem', fontWeight:700, color }}>
141
+ {isNeutral ? (found ? 'Neutral' : 'No Data') : (value > 0 ? '+' : '') + value.toFixed(2)}
142
+ </span>
143
  </div>
144
  <div className="sentiment-bar-track">
145
  <div className="sentiment-bar-fill" style={{ width:`${pct}%`, background:color }}/>
 
493
  {result.nlp_cached && <span className="badge badge-blue">Cached</span>}
494
  </div>
495
  <div style={{ display:'flex', flexDirection:'column', gap:14 }}>
496
+ <SentimentScore label="Injury / Availability" icon="🏥" value={result.nlp_results.durability} found={result.nlp_found}/>
497
+ <SentimentScore label="Recent Form & Impact" icon="📈" value={result.nlp_results.recency} found={result.nlp_found}/>
498
+ <SentimentScore label="Transfer Speculation" icon="🗞️" value={result.nlp_results.agent} found={result.nlp_found}/>
499
  </div>
500
  <div style={{ marginTop:16 }}>
501
  <button className="btn btn-ghost" style={{ fontSize:'0.78rem', padding:'6px 14px' }}
fairvalue-webapp/src/pages/Intel.tsx CHANGED
@@ -12,11 +12,12 @@ interface ScoutResult {
12
  agent: number
13
  logs: string[]
14
  from_cache: boolean
 
15
  }
16
 
17
  function ScoreCard({
18
- label, value, icon, desc,
19
- }: { label: string; value: number; icon: string; desc: string }) {
20
  const norm = (value + 1) / 2 // –1..+1 → 0..1
21
  const pct = Math.round(norm * 100)
22
  const color = value > 0.1 ? 'var(--profit-color)' : value < -0.1 ? 'var(--loss-color)' : 'var(--accent-blue)'
@@ -33,9 +34,11 @@ function ScoreCard({
33
  </div>
34
  <div style={{ textAlign:'right' }}>
35
  <div style={{ fontSize:'2rem', fontWeight:900, color, letterSpacing:'-0.03em', lineHeight:1 }}>
36
- {value > 0 ? '+' : ''}{value.toFixed(2)}
37
  </div>
38
- <span className={`badge ${badge}`} style={{ marginTop:6 }}>{label2}</span>
 
 
39
  </div>
40
  </div>
41
  {/* Bar */}
@@ -198,16 +201,19 @@ export default function Intel() {
198
  <ScoreCard
199
  icon="🏥" label="Durability"
200
  value={result.durability}
 
201
  desc="Based on injury reports, games missed, and fitness news. Negative = active concerns."
202
  />
203
  <ScoreCard
204
  icon="📈" label="Recent Form"
205
  value={result.recency}
 
206
  desc="Based on match ratings, goal/assist data, and performance commentary."
207
  />
208
  <ScoreCard
209
  icon="🗞️" label="Transfer Heat"
210
  value={result.agent}
 
211
  desc="Based on rumour intensity, agent activity, and bid speculation volume."
212
  />
213
  </div>
 
12
  agent: number
13
  logs: string[]
14
  from_cache: boolean
15
+ nlp_found?: boolean
16
  }
17
 
18
  function ScoreCard({
19
+ label, value, icon, desc, found
20
+ }: { label: string; value: number; icon: string; desc: string; found?: boolean }) {
21
  const norm = (value + 1) / 2 // –1..+1 → 0..1
22
  const pct = Math.round(norm * 100)
23
  const color = value > 0.1 ? 'var(--profit-color)' : value < -0.1 ? 'var(--loss-color)' : 'var(--accent-blue)'
 
34
  </div>
35
  <div style={{ textAlign:'right' }}>
36
  <div style={{ fontSize:'2rem', fontWeight:900, color, letterSpacing:'-0.03em', lineHeight:1 }}>
37
+ {value === 0 ? (found ? '0.00' : 'N/A') : (value > 0 ? '+' : '') + value.toFixed(2)}
38
  </div>
39
+ <span className={`badge ${badge}`} style={{ marginTop:6 }}>
40
+ {value === 0 ? (found ? 'Neutral' : 'No Data') : label2}
41
+ </span>
42
  </div>
43
  </div>
44
  {/* Bar */}
 
201
  <ScoreCard
202
  icon="🏥" label="Durability"
203
  value={result.durability}
204
+ found={result.nlp_found}
205
  desc="Based on injury reports, games missed, and fitness news. Negative = active concerns."
206
  />
207
  <ScoreCard
208
  icon="📈" label="Recent Form"
209
  value={result.recency}
210
+ found={result.nlp_found}
211
  desc="Based on match ratings, goal/assist data, and performance commentary."
212
  />
213
  <ScoreCard
214
  icon="🗞️" label="Transfer Heat"
215
  value={result.agent}
216
+ found={result.nlp_found}
217
  desc="Based on rumour intensity, agent activity, and bid speculation volume."
218
  />
219
  </div>