Spaces:
Sleeping
Sleeping
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 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 & 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 & 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
|
|
|
|
|
|
|
| 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 }}>
|
|
|
|
|
|
|
| 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 ? '+' : ''
|
| 37 |
</div>
|
| 38 |
-
<span className={`badge ${badge}`} style={{ marginTop:6 }}>
|
|
|
|
|
|
|
| 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>
|