@semanticintent/semantic-chirp-intelligence-mcp 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/authenticate.js +207 -0
- package/build/analyses/BreakoutAnalysis.js +386 -0
- package/build/analyses/GamesInHandAnalysis.js +257 -0
- package/build/analyses/IceAnalysis.js +316 -0
- package/build/analyses/LineupAnalysis.js +284 -0
- package/build/analyses/StreamingAnalysis.js +246 -0
- package/build/analyses/WeekendStreamAnalysis.js +599 -0
- package/build/config/chirp-styles.js +36 -0
- package/build/config/personality-modes.js +36 -0
- package/build/config/tool-metadata.js +113 -0
- package/build/domain/governance.js +322 -0
- package/build/domain/types.js +14 -0
- package/build/experimental/semantic-breakout-tool.js +188 -0
- package/build/experimental/semantic-intent-parser.js +222 -0
- package/build/experimental/semantic-tool-integration.js +146 -0
- package/build/experimental/test-parser.js +61 -0
- package/build/index.js +1549 -0
- package/build/services/ChirpIntelligence.js +213 -0
- package/build/services/YahooApiClient.js +309 -0
- package/build/template/AnalysisTemplate.js +167 -0
- package/build/types.js +2 -0
- package/package.json +50 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🏒 Breakout Player Analysis - Fantasy Hockey Intelligence
|
|
3
|
+
*
|
|
4
|
+
* Analyzes free agents to identify top pickups and breakout candidates
|
|
5
|
+
* using data-driven predictability with external source integration.
|
|
6
|
+
*
|
|
7
|
+
* Based on comprehensive prompt template with:
|
|
8
|
+
* - 40% recent performance
|
|
9
|
+
* - 30% projections
|
|
10
|
+
* - 20% opportunity metrics
|
|
11
|
+
* - 10% risk factors
|
|
12
|
+
*/
|
|
13
|
+
import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
|
|
14
|
+
import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
|
|
15
|
+
export class BreakoutAnalysis extends AnalysisTemplate {
|
|
16
|
+
yahooClient;
|
|
17
|
+
leagueId;
|
|
18
|
+
teamId;
|
|
19
|
+
constructor(yahooClient, leagueId, teamId) {
|
|
20
|
+
super('get_breakout_analysis', 'streaming_recommendations');
|
|
21
|
+
this.yahooClient = yahooClient;
|
|
22
|
+
this.leagueId = leagueId;
|
|
23
|
+
this.teamId = teamId;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetch raw data: free agents, trending players, roster
|
|
27
|
+
*/
|
|
28
|
+
async fetchData(args) {
|
|
29
|
+
const ownershipThreshold = args.ownership_threshold || 50;
|
|
30
|
+
// Fetch free agents across all positions
|
|
31
|
+
const positions = args.position_filter || ['C', 'LW', 'RW', 'D', 'G'];
|
|
32
|
+
const freeAgentPromises = positions.map(pos => this.yahooClient.searchPlayers(pos, 50, this.leagueId));
|
|
33
|
+
const [freeAgentResults, trendingAdds, roster] = await Promise.all([
|
|
34
|
+
Promise.all(freeAgentPromises),
|
|
35
|
+
this.yahooClient.getTrendingPlayers('add', 25, this.leagueId),
|
|
36
|
+
this.yahooClient.getTeamRoster(this.leagueId, this.teamId)
|
|
37
|
+
]);
|
|
38
|
+
// Combine and deduplicate free agents
|
|
39
|
+
const allFreeAgents = [];
|
|
40
|
+
for (const result of freeAgentResults) {
|
|
41
|
+
if (result.players) {
|
|
42
|
+
allFreeAgents.push(...result.players);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Remove duplicates and filter by ownership
|
|
46
|
+
const uniqueFreeAgents = Array.from(new Map(allFreeAgents.map(p => [p.player_id, p])).values()).filter(p => (p.percent_owned || 0) < ownershipThreshold);
|
|
47
|
+
return {
|
|
48
|
+
freeAgents: uniqueFreeAgents,
|
|
49
|
+
trendingAdds: trendingAdds.players || [],
|
|
50
|
+
roster
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Prepare data for analysis
|
|
55
|
+
*/
|
|
56
|
+
async prepareData(rawData, args) {
|
|
57
|
+
return {
|
|
58
|
+
availablePlayers: rawData.freeAgents,
|
|
59
|
+
trendingPlayers: rawData.trendingAdds,
|
|
60
|
+
roster: rawData.roster
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Execute breakout analysis with predictable scoring
|
|
65
|
+
*/
|
|
66
|
+
async analyzeData(data, args) {
|
|
67
|
+
const freeAgents = data.availablePlayers || [];
|
|
68
|
+
const trending = data.trendingPlayers || [];
|
|
69
|
+
// Score each free agent using the comprehensive formula
|
|
70
|
+
const scoredPlayers = await Promise.all(freeAgents.map(player => this.scorePlayer(player, trending)));
|
|
71
|
+
// Sort by score and categorize
|
|
72
|
+
const sorted = scoredPlayers
|
|
73
|
+
.filter(p => p.breakout_score >= (args.min_score || 0))
|
|
74
|
+
.sort((a, b) => b.breakout_score - a.breakout_score);
|
|
75
|
+
// Separate into pickups vs breakouts
|
|
76
|
+
const pickups = this.identifyTopPickups(sorted, args.max_results || 10);
|
|
77
|
+
const breakouts = this.identifyBreakoutCandidates(sorted, args.breakout_age_max || 26);
|
|
78
|
+
return {
|
|
79
|
+
pickups,
|
|
80
|
+
breakouts,
|
|
81
|
+
all_scored: sorted.slice(0, 50),
|
|
82
|
+
position_breakdown: this.analyzeByPosition(sorted),
|
|
83
|
+
market_intelligence: this.analyzeMarketTrends(trending, sorted)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Score a player using the comprehensive formula:
|
|
88
|
+
* Score = (0.4 * Recent PPG) + (0.3 * Proj FP/G) + (0.2 * Opp Score/10) - (0.1 * Risk %)
|
|
89
|
+
*/
|
|
90
|
+
async scorePlayer(player, trending) {
|
|
91
|
+
// Get detailed stats for scoring
|
|
92
|
+
const stats = await this.getPlayerMetrics(player);
|
|
93
|
+
// Recent performance (0-100 scale)
|
|
94
|
+
const recentPPG = this.calculateRecentPerformance(stats) * 100;
|
|
95
|
+
// Projected fantasy points (0-100 scale, estimated)
|
|
96
|
+
const projectedFPG = this.estimateProjectedPoints(player, stats, trending) * 100;
|
|
97
|
+
// Opportunity score (0-100 scale)
|
|
98
|
+
const opportunityScore = this.calculateOpportunity(player, stats);
|
|
99
|
+
// Risk percentage (0-100)
|
|
100
|
+
const riskPercentage = this.calculateRisk(player, stats);
|
|
101
|
+
// Apply formula
|
|
102
|
+
const breakoutScore = 0.4 * recentPPG +
|
|
103
|
+
0.3 * projectedFPG +
|
|
104
|
+
0.2 * opportunityScore -
|
|
105
|
+
0.1 * riskPercentage;
|
|
106
|
+
// Determine confidence and category
|
|
107
|
+
const confidence = this.determineConfidence(breakoutScore, riskPercentage);
|
|
108
|
+
const category = this.categorizePlayer(breakoutScore);
|
|
109
|
+
const catalyst = this.identifyCatalyst(player, stats, trending);
|
|
110
|
+
return {
|
|
111
|
+
...player,
|
|
112
|
+
breakout_score: Math.round(breakoutScore),
|
|
113
|
+
recent_ppg: recentPPG / 100,
|
|
114
|
+
projected_fpg: projectedFPG / 100,
|
|
115
|
+
opportunity_score: opportunityScore,
|
|
116
|
+
risk_percentage: riskPercentage,
|
|
117
|
+
catalyst,
|
|
118
|
+
confidence,
|
|
119
|
+
category
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Calculate recent performance score
|
|
124
|
+
*/
|
|
125
|
+
calculateRecentPerformance(stats) {
|
|
126
|
+
// Simplified - in real implementation, would fetch last 5-10 games
|
|
127
|
+
// For now, use season averages as proxy
|
|
128
|
+
const goals = parseFloat(stats.G || 0);
|
|
129
|
+
const assists = parseFloat(stats.A || 0);
|
|
130
|
+
const gamesPlayed = parseFloat(stats.GP || 1);
|
|
131
|
+
if (gamesPlayed === 0)
|
|
132
|
+
return 0;
|
|
133
|
+
const ppg = (goals + assists) / gamesPlayed;
|
|
134
|
+
return Math.min(ppg, 1.5); // Cap at 1.5 PPG
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Estimate projected fantasy points
|
|
138
|
+
*/
|
|
139
|
+
estimateProjectedPoints(player, stats, trending) {
|
|
140
|
+
// Base projection on current stats + trending momentum
|
|
141
|
+
const isTrending = trending.some(t => t.player_id === player.player_id);
|
|
142
|
+
const baseProjection = this.calculateRecentPerformance(stats);
|
|
143
|
+
const trendingBonus = isTrending ? 0.15 : 0;
|
|
144
|
+
return Math.min(baseProjection + trendingBonus, 1.0);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Calculate opportunity score (TOI, PP role, linemates)
|
|
148
|
+
*/
|
|
149
|
+
calculateOpportunity(player, stats) {
|
|
150
|
+
let score = 50; // Base score
|
|
151
|
+
// Position-based opportunities
|
|
152
|
+
if (player.position.includes('C'))
|
|
153
|
+
score += 10; // Centers have more opportunity
|
|
154
|
+
if (player.position.includes('LW') || player.position.includes('RW'))
|
|
155
|
+
score += 5;
|
|
156
|
+
// Check if on good team (more goals = more opportunities)
|
|
157
|
+
const teamScore = this.getTeamStrength(player.team);
|
|
158
|
+
score += teamScore;
|
|
159
|
+
return Math.min(score, 100);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Calculate risk percentage
|
|
163
|
+
*/
|
|
164
|
+
calculateRisk(player, stats) {
|
|
165
|
+
let risk = 20; // Base risk
|
|
166
|
+
// Injury status increases risk
|
|
167
|
+
if (player.status && player.status !== '') {
|
|
168
|
+
risk += 30;
|
|
169
|
+
}
|
|
170
|
+
// Low games played = higher risk
|
|
171
|
+
const gamesPlayed = parseFloat(stats.GP || 0);
|
|
172
|
+
if (gamesPlayed < 10)
|
|
173
|
+
risk += 20;
|
|
174
|
+
// High ownership = lower risk (proven commodity)
|
|
175
|
+
const ownership = player.percent_owned || 0;
|
|
176
|
+
if (ownership > 30)
|
|
177
|
+
risk -= 10;
|
|
178
|
+
if (ownership < 10)
|
|
179
|
+
risk += 15;
|
|
180
|
+
return Math.min(Math.max(risk, 0), 100);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get team strength score
|
|
184
|
+
*/
|
|
185
|
+
getTeamStrength(teamAbbr) {
|
|
186
|
+
// Simplified team rankings - top teams get bonus
|
|
187
|
+
const topTeams = ['BOS', 'CAR', 'COL', 'DAL', 'EDM', 'FLA', 'NYR', 'TOR', 'VGK', 'WPG'];
|
|
188
|
+
const midTeams = ['CGY', 'LAK', 'MIN', 'NJD', 'NSH', 'NYI', 'SEA', 'TBL', 'VAN'];
|
|
189
|
+
if (topTeams.includes(teamAbbr))
|
|
190
|
+
return 20;
|
|
191
|
+
if (midTeams.includes(teamAbbr))
|
|
192
|
+
return 10;
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Identify catalyst for breakout potential
|
|
197
|
+
*/
|
|
198
|
+
identifyCatalyst(player, stats, trending) {
|
|
199
|
+
const isTrending = trending.some(t => t.player_id === player.player_id);
|
|
200
|
+
if (isTrending)
|
|
201
|
+
return 'Hot streak - trending upward';
|
|
202
|
+
if (player.position.includes('C'))
|
|
203
|
+
return 'Top-6 center opportunity';
|
|
204
|
+
if (this.getTeamStrength(player.team) >= 20)
|
|
205
|
+
return 'Playing on elite team';
|
|
206
|
+
return 'Solid opportunity available';
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Determine confidence level
|
|
210
|
+
*/
|
|
211
|
+
determineConfidence(score, risk) {
|
|
212
|
+
if (score >= 70 && risk < 30)
|
|
213
|
+
return 'high';
|
|
214
|
+
if (score >= 50 && risk < 50)
|
|
215
|
+
return 'medium';
|
|
216
|
+
return 'low';
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Categorize player by score
|
|
220
|
+
*/
|
|
221
|
+
categorizePlayer(score) {
|
|
222
|
+
if (score >= 80)
|
|
223
|
+
return 'must_add';
|
|
224
|
+
if (score >= 65)
|
|
225
|
+
return 'strong_pickup';
|
|
226
|
+
if (score >= 50)
|
|
227
|
+
return 'monitor';
|
|
228
|
+
return 'sleeper';
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Get player metrics (mock implementation)
|
|
232
|
+
*/
|
|
233
|
+
async getPlayerMetrics(player) {
|
|
234
|
+
// In real implementation, fetch detailed stats
|
|
235
|
+
// For now, return mock data
|
|
236
|
+
return {
|
|
237
|
+
G: Math.random() * 30,
|
|
238
|
+
A: Math.random() * 40,
|
|
239
|
+
GP: 50 + Math.random() * 30,
|
|
240
|
+
PPP: Math.random() * 20,
|
|
241
|
+
SOG: Math.random() * 150
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Identify top pickup recommendations
|
|
246
|
+
*/
|
|
247
|
+
identifyTopPickups(sorted, maxResults) {
|
|
248
|
+
return sorted
|
|
249
|
+
.filter(p => p.category === 'must_add' || p.category === 'strong_pickup')
|
|
250
|
+
.slice(0, maxResults)
|
|
251
|
+
.map(player => ({
|
|
252
|
+
...player,
|
|
253
|
+
pickup_score: player.breakout_score,
|
|
254
|
+
urgency: player.category === 'must_add' ? 'immediate' : 'high',
|
|
255
|
+
fit_reason: `${player.catalyst} - Score: ${player.breakout_score}`,
|
|
256
|
+
streaming_score: player.breakout_score,
|
|
257
|
+
reason: player.catalyst
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Identify breakout candidates (young players with upside)
|
|
262
|
+
*/
|
|
263
|
+
identifyBreakoutCandidates(sorted, ageMax) {
|
|
264
|
+
// In real implementation, would filter by age
|
|
265
|
+
// For now, return top sleepers/monitors
|
|
266
|
+
return sorted
|
|
267
|
+
.filter(p => p.category === 'sleeper' || p.category === 'monitor')
|
|
268
|
+
.filter(p => p.confidence === 'medium' || p.confidence === 'high')
|
|
269
|
+
.slice(0, 5);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Analyze players by position
|
|
273
|
+
*/
|
|
274
|
+
analyzeByPosition(players) {
|
|
275
|
+
const positions = ['C', 'LW', 'RW', 'D', 'G'];
|
|
276
|
+
const breakdown = {};
|
|
277
|
+
for (const pos of positions) {
|
|
278
|
+
const posPlayers = players.filter(p => p.position.includes(pos));
|
|
279
|
+
breakdown[pos] = {
|
|
280
|
+
count: posPlayers.length,
|
|
281
|
+
top_player: posPlayers[0] || null,
|
|
282
|
+
avg_score: posPlayers.length > 0
|
|
283
|
+
? posPlayers.reduce((sum, p) => sum + p.breakout_score, 0) / posPlayers.length
|
|
284
|
+
: 0
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return breakdown;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Analyze market trends
|
|
291
|
+
*/
|
|
292
|
+
analyzeMarketTrends(trending, scored) {
|
|
293
|
+
const trendingScored = scored.filter(s => trending.some(t => t.player_id === s.player_id));
|
|
294
|
+
return {
|
|
295
|
+
trending_count: trending.length,
|
|
296
|
+
trending_avg_score: trendingScored.length > 0
|
|
297
|
+
? trendingScored.reduce((sum, p) => sum + p.breakout_score, 0) / trendingScored.length
|
|
298
|
+
: 0,
|
|
299
|
+
hot_positions: this.identifyHotPositions(trending)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Identify hot positions from trending data
|
|
304
|
+
*/
|
|
305
|
+
identifyHotPositions(trending) {
|
|
306
|
+
const positionCounts = {};
|
|
307
|
+
for (const player of trending) {
|
|
308
|
+
const positions = (player.position || '').split(',');
|
|
309
|
+
for (const pos of positions) {
|
|
310
|
+
positionCounts[pos] = (positionCounts[pos] || 0) + 1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return Object.entries(positionCounts)
|
|
314
|
+
.sort(([, a], [, b]) => b - a)
|
|
315
|
+
.slice(0, 3)
|
|
316
|
+
.map(([pos]) => pos);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Generate chirp intelligence
|
|
320
|
+
*/
|
|
321
|
+
async generateChirp(analysisResults, semanticContract, data) {
|
|
322
|
+
if (semanticContract.enable_chirp === false) {
|
|
323
|
+
return analysisResults;
|
|
324
|
+
}
|
|
325
|
+
// Use ChirpIntelligence static enhance method
|
|
326
|
+
const enhanced = ChirpIntelligence.enhance('get_breakout_analysis', {
|
|
327
|
+
...analysisResults,
|
|
328
|
+
streaming_targets: analysisResults.pickups,
|
|
329
|
+
market_intelligence: analysisResults.market_intelligence,
|
|
330
|
+
recommendations: analysisResults.pickups
|
|
331
|
+
}, semanticContract);
|
|
332
|
+
return enhanced;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Format final response
|
|
336
|
+
*/
|
|
337
|
+
async formatResponse(chirpEnhanced, data) {
|
|
338
|
+
const recommendations = chirpEnhanced.pickups.map((p) => ({
|
|
339
|
+
priority: p.urgency === 'immediate' ? 'CRITICAL' : 'HIGH',
|
|
340
|
+
action: 'pickup',
|
|
341
|
+
player: p,
|
|
342
|
+
reasoning: p.fit_reason
|
|
343
|
+
}));
|
|
344
|
+
const insights = {
|
|
345
|
+
streaming_targets: chirpEnhanced.pickups,
|
|
346
|
+
favorable_teams: Object.entries(chirpEnhanced.position_breakdown)
|
|
347
|
+
.map(([pos, data]) => ({
|
|
348
|
+
team_abbr: pos,
|
|
349
|
+
games_count: data.count,
|
|
350
|
+
favorable_score: data.avg_score
|
|
351
|
+
})),
|
|
352
|
+
market_intelligence: {
|
|
353
|
+
total_trending: chirpEnhanced.market_intelligence.trending_count,
|
|
354
|
+
favorable_teams_count: Object.keys(chirpEnhanced.position_breakdown).length,
|
|
355
|
+
top_trending_team: chirpEnhanced.market_intelligence.hot_positions[0] || 'N/A'
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
analysis_insights: insights,
|
|
360
|
+
recommendations,
|
|
361
|
+
chirp_intelligence: chirpEnhanced.chirp_intelligence || this.getDefaultChirp(),
|
|
362
|
+
metadata: {
|
|
363
|
+
analysis_type: this.analysisType,
|
|
364
|
+
tool_identity: 'breakout_analysis',
|
|
365
|
+
generated_at: new Date().toISOString(),
|
|
366
|
+
semantic_contract_applied: true
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Default chirp when chirp intelligence is disabled
|
|
372
|
+
*/
|
|
373
|
+
getDefaultChirp() {
|
|
374
|
+
return {
|
|
375
|
+
tool_identity: 'breakout_analysis',
|
|
376
|
+
style: 'analytical',
|
|
377
|
+
personality: 'data_driven',
|
|
378
|
+
intensity: 'standard',
|
|
379
|
+
semantic_context: 'breakout_player_analysis',
|
|
380
|
+
analysis_chirp: 'Data-driven breakout analysis complete',
|
|
381
|
+
intent_summary: 'Breakout player recommendations ready',
|
|
382
|
+
ice_cold_truth: 'Smart pickups win championships',
|
|
383
|
+
energy_level: 'focused'
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* GamesInHandAnalysis - Template Method Pattern Implementation
|
|
4
|
+
*
|
|
5
|
+
* Analyzes schedule advantages by calculating games remaining differentials
|
|
6
|
+
* between your team and opponents.
|
|
7
|
+
*/
|
|
8
|
+
import { AnalysisTemplate } from '../template/AnalysisTemplate.js';
|
|
9
|
+
import { ChirpIntelligence } from '../services/ChirpIntelligence.js';
|
|
10
|
+
export class GamesInHandAnalysis extends AnalysisTemplate {
|
|
11
|
+
apiClient;
|
|
12
|
+
leagueId;
|
|
13
|
+
teamId;
|
|
14
|
+
constructor(apiClient, leagueId, teamId) {
|
|
15
|
+
super("get_games_in_hand", "schedule_advantage");
|
|
16
|
+
this.apiClient = apiClient;
|
|
17
|
+
this.leagueId = leagueId;
|
|
18
|
+
this.teamId = teamId;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Hook 1: Fetch raw data from Yahoo API
|
|
22
|
+
*/
|
|
23
|
+
async fetchData(args) {
|
|
24
|
+
try {
|
|
25
|
+
// Fetch current matchup data
|
|
26
|
+
const matchupData = await this.apiClient.getTeamMatchup(this.leagueId, this.teamId);
|
|
27
|
+
// Fetch league scoreboard for schedule data
|
|
28
|
+
const scoreboardData = await this.apiClient.getLeagueScoreboard(this.leagueId);
|
|
29
|
+
return {
|
|
30
|
+
matchup: matchupData,
|
|
31
|
+
scoreboard: scoreboardData
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new Error(`Failed to fetch games in hand data: ${error}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Hook 2: Prepare data into FantasyData structure
|
|
40
|
+
*/
|
|
41
|
+
async prepareData(rawData, args) {
|
|
42
|
+
const { matchup, scoreboard } = rawData;
|
|
43
|
+
// Parse matchup to find opponent - try multiple potential structures
|
|
44
|
+
let teams = matchup.fantasy_content?.team?.[1]?.matchup?.['0']?.teams?.['0']?.team || [];
|
|
45
|
+
// If teams is empty, try alternative structure (matchups array)
|
|
46
|
+
if (teams.length === 0) {
|
|
47
|
+
const matchups = matchup.fantasy_content?.team?.[1]?.matchups;
|
|
48
|
+
if (matchups && matchups.count !== '0') {
|
|
49
|
+
// Find the current matchup where status === "midevent"
|
|
50
|
+
const matchupKeys = Object.keys(matchups).filter(key => key !== 'count');
|
|
51
|
+
const currentMatchupKey = matchupKeys.find(key => {
|
|
52
|
+
const matchupData = matchups[key]?.matchup?.[0] || matchups[key]?.matchup;
|
|
53
|
+
return matchupData?.status === 'midevent';
|
|
54
|
+
});
|
|
55
|
+
// Use current matchup if found, otherwise fallback to last matchup
|
|
56
|
+
const selectedKey = currentMatchupKey || matchupKeys[matchupKeys.length - 1];
|
|
57
|
+
if (selectedKey && matchups[selectedKey]?.matchup?.[0]) {
|
|
58
|
+
teams = matchups[selectedKey].matchup[0].teams?.['0']?.team || [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Handle both full team IDs (nhl.l.123.t.1) and partial (just the number)
|
|
63
|
+
const cleanLeagueId = this.leagueId.replace(/^nhl\.l\./, '');
|
|
64
|
+
const cleanTeamId = this.teamId.replace(/^.*\.t\./, '');
|
|
65
|
+
const fullTeamKey = `nhl.l.${cleanLeagueId}.t.${cleanTeamId}`;
|
|
66
|
+
const yourTeam = teams.find((t) => {
|
|
67
|
+
// Team data can be nested as an array or direct object
|
|
68
|
+
const teamData = Array.isArray(t) ? t[0] : t;
|
|
69
|
+
const teamKey = teamData?.team_key;
|
|
70
|
+
return teamKey === fullTeamKey || teamKey === this.teamId || teamKey?.endsWith(`.t.${cleanTeamId}`);
|
|
71
|
+
});
|
|
72
|
+
const opponentTeam = teams.find((t) => {
|
|
73
|
+
const teamData = Array.isArray(t) ? t[0] : t;
|
|
74
|
+
const teamKey = teamData?.team_key;
|
|
75
|
+
return teamKey && teamKey !== fullTeamKey && !teamKey?.endsWith(`.t.${cleanTeamId}`);
|
|
76
|
+
});
|
|
77
|
+
if (!yourTeam || !opponentTeam) {
|
|
78
|
+
throw new Error(`Could not find matchup data. Teams found: ${teams.length}, Structure: ${JSON.stringify(matchup.fantasy_content?.team?.[1], null, 2).substring(0, 500)}`);
|
|
79
|
+
}
|
|
80
|
+
// Extract team data (handle both array and object structures)
|
|
81
|
+
const yourTeamData = Array.isArray(yourTeam) ? yourTeam[0] : yourTeam;
|
|
82
|
+
const opponentTeamData = Array.isArray(opponentTeam) ? opponentTeam[0] : opponentTeam;
|
|
83
|
+
// Extract team rosters
|
|
84
|
+
const yourRoster = yourTeamData.roster?.players || [];
|
|
85
|
+
const opponentRoster = opponentTeamData.roster?.players || [];
|
|
86
|
+
// Parse player data using .find() pattern like IceAnalysis and LineupAnalysis
|
|
87
|
+
const parsePlayer = (playerData) => {
|
|
88
|
+
const player = playerData.player?.[0] || playerData;
|
|
89
|
+
// Use .find() pattern since player is an array of property objects
|
|
90
|
+
const player_id = Array.isArray(player)
|
|
91
|
+
? player.find((item) => item.player_id)?.player_id
|
|
92
|
+
: player.player_id;
|
|
93
|
+
const name = Array.isArray(player)
|
|
94
|
+
? player.find((item) => item.name)?.name?.full
|
|
95
|
+
: player.name?.full;
|
|
96
|
+
const position = Array.isArray(player)
|
|
97
|
+
? player.find((item) => item.display_position)?.display_position ||
|
|
98
|
+
player.find((item) => item.primary_positions)?.primary_positions?.[0]
|
|
99
|
+
: player.display_position || player.primary_positions?.[0];
|
|
100
|
+
const team = Array.isArray(player)
|
|
101
|
+
? player.find((item) => item.editorial_team_abbr)?.editorial_team_abbr
|
|
102
|
+
: player.editorial_team_abbr;
|
|
103
|
+
const status = Array.isArray(player)
|
|
104
|
+
? player.find((item) => item.status)?.status
|
|
105
|
+
: player.status;
|
|
106
|
+
const selected_position = Array.isArray(player)
|
|
107
|
+
? player.find((item) => item.selected_position)?.selected_position
|
|
108
|
+
: player.selected_position;
|
|
109
|
+
return {
|
|
110
|
+
player_id: player_id || '',
|
|
111
|
+
name: name || 'Unknown',
|
|
112
|
+
position: position || 'Unknown',
|
|
113
|
+
team: team || '',
|
|
114
|
+
selected_position: selected_position || [],
|
|
115
|
+
status: status || ''
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
const yourPlayers = yourRoster.map(parsePlayer);
|
|
119
|
+
const opponentPlayers = opponentRoster.map(parsePlayer);
|
|
120
|
+
return {
|
|
121
|
+
roster: {
|
|
122
|
+
team_key: yourTeamData.team_key || '',
|
|
123
|
+
team_name: yourTeamData.name || 'Your Team',
|
|
124
|
+
players: yourPlayers
|
|
125
|
+
},
|
|
126
|
+
opponent: {
|
|
127
|
+
team_key: opponentTeamData.team_key || '',
|
|
128
|
+
team_name: opponentTeamData.name || 'Opponent',
|
|
129
|
+
players: opponentPlayers
|
|
130
|
+
},
|
|
131
|
+
matchup: {
|
|
132
|
+
week: matchup.fantasy_content?.team?.[0]?.matchups?.['0']?.matchup?.week || 'current',
|
|
133
|
+
your_team_key: yourTeamData.team_key || '',
|
|
134
|
+
opponent_team_key: opponentTeamData.team_key || ''
|
|
135
|
+
},
|
|
136
|
+
scoreboard: scoreboard
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Hook 3: Analyze data to calculate games in hand
|
|
141
|
+
*/
|
|
142
|
+
async analyzeData(data, args) {
|
|
143
|
+
const lookAheadDays = args.look_ahead_days || 7;
|
|
144
|
+
// Calculate games remaining for each team
|
|
145
|
+
const yourGamesRemaining = this.calculateGamesRemaining(data.roster.players, lookAheadDays);
|
|
146
|
+
const opponentGamesRemaining = this.calculateGamesRemaining(data.opponent.players || [], lookAheadDays);
|
|
147
|
+
const advantage = yourGamesRemaining - opponentGamesRemaining;
|
|
148
|
+
// Generate strategic recommendation based on advantage
|
|
149
|
+
let strategicRecommendation;
|
|
150
|
+
if (advantage > 5) {
|
|
151
|
+
strategicRecommendation = "MASSIVE ADVANTAGE: Maximize starts to dominate volume categories";
|
|
152
|
+
}
|
|
153
|
+
else if (advantage > 2) {
|
|
154
|
+
strategicRecommendation = "SIGNIFICANT ADVANTAGE: Stream aggressively to capitalize";
|
|
155
|
+
}
|
|
156
|
+
else if (advantage > 0) {
|
|
157
|
+
strategicRecommendation = "SLIGHT ADVANTAGE: Focus on quality streaming targets";
|
|
158
|
+
}
|
|
159
|
+
else if (advantage === 0) {
|
|
160
|
+
strategicRecommendation = "EVEN MATCHUP: Focus on roster optimization over volume";
|
|
161
|
+
}
|
|
162
|
+
else if (advantage > -3) {
|
|
163
|
+
strategicRecommendation = "SLIGHT DISADVANTAGE: Prioritize high-quality starts";
|
|
164
|
+
}
|
|
165
|
+
else if (advantage > -6) {
|
|
166
|
+
strategicRecommendation = "SIGNIFICANT DISADVANTAGE: Focus on efficiency, avoid streaming busts";
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
strategicRecommendation = "MASSIVE DISADVANTAGE: Quality over quantity - pick your spots carefully";
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
your_team: {
|
|
173
|
+
team_name: data.roster.team_name,
|
|
174
|
+
games_remaining: yourGamesRemaining,
|
|
175
|
+
players_with_games: data.roster.players.filter(p => !p.selected_position.includes('IR'))
|
|
176
|
+
},
|
|
177
|
+
opponent: {
|
|
178
|
+
team_name: data.opponent.team_name || 'Opponent',
|
|
179
|
+
games_remaining: opponentGamesRemaining,
|
|
180
|
+
players_with_games: (data.opponent.players || []).filter((p) => !p.selected_position.includes('IR'))
|
|
181
|
+
},
|
|
182
|
+
advantage,
|
|
183
|
+
strategic_recommendation: strategicRecommendation
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Hook 4: Generate chirp-enhanced response
|
|
188
|
+
*/
|
|
189
|
+
async generateChirp(analysisResults, semanticContract, data) {
|
|
190
|
+
return ChirpIntelligence.enhance(this.toolName, analysisResults, semanticContract);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Hook 5: Format final response
|
|
194
|
+
*/
|
|
195
|
+
async formatResponse(chirpEnhanced, data) {
|
|
196
|
+
const analysis = chirpEnhanced;
|
|
197
|
+
// Create recommendations based on advantage
|
|
198
|
+
const recommendations = [];
|
|
199
|
+
if (analysis.advantage > 2) {
|
|
200
|
+
recommendations.push({
|
|
201
|
+
priority: "HIGH",
|
|
202
|
+
action: "volume_play",
|
|
203
|
+
reasoning: `You have ${analysis.advantage} more games than opponent - stream aggressively`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
else if (analysis.advantage < -2) {
|
|
207
|
+
recommendations.push({
|
|
208
|
+
priority: "HIGH",
|
|
209
|
+
action: "bench_upgrade",
|
|
210
|
+
reasoning: `Opponent has ${Math.abs(analysis.advantage)} more games - focus on quality over quantity`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
recommendations.push({
|
|
215
|
+
priority: "MEDIUM",
|
|
216
|
+
action: "lineup_change",
|
|
217
|
+
reasoning: "Games are even - optimize lineup for quality matchups"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const analysisInsights = {
|
|
221
|
+
schedule_advantage: {
|
|
222
|
+
your_games: analysis.your_team.games_remaining,
|
|
223
|
+
opponent_games: analysis.opponent.games_remaining,
|
|
224
|
+
net_advantage: analysis.advantage,
|
|
225
|
+
strategic_impact: analysis.strategic_recommendation
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const metadata = {
|
|
229
|
+
analysis_type: this.analysisType,
|
|
230
|
+
timestamp: new Date().toISOString(),
|
|
231
|
+
team_context: {
|
|
232
|
+
team_name: data.roster.team_name,
|
|
233
|
+
opponent_name: data.opponent.team_name
|
|
234
|
+
},
|
|
235
|
+
semantic_contract_applied: true
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
analysis_insights: analysisInsights,
|
|
239
|
+
recommendations,
|
|
240
|
+
chirp_intelligence: analysis.chirp_intelligence,
|
|
241
|
+
metadata
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Helper: Calculate total games remaining for a roster
|
|
246
|
+
*/
|
|
247
|
+
calculateGamesRemaining(players, lookAheadDays) {
|
|
248
|
+
// This is a simplified calculation
|
|
249
|
+
// In production, would parse actual NHL schedule data from scoreboard
|
|
250
|
+
// Active players (not on IR) typically play 3-4 games per week
|
|
251
|
+
const activePlayers = players.filter(p => !p.selected_position.includes('IR') &&
|
|
252
|
+
!p.selected_position.includes('BN'));
|
|
253
|
+
// Rough estimate: 3.5 games per active player per week
|
|
254
|
+
const weeksInLookAhead = lookAheadDays / 7;
|
|
255
|
+
return Math.round(activePlayers.length * 3.5 * weeksInLookAhead);
|
|
256
|
+
}
|
|
257
|
+
}
|