@gotza02/sequential-thinking 10000.0.1 → 10000.0.2
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/README.md +6 -0
- package/dist/tools/sports/core/constants.d.ts +2 -1
- package/dist/tools/sports/core/constants.js +18 -6
- package/dist/tools/sports/providers/scraper.d.ts +6 -1
- package/dist/tools/sports/providers/scraper.js +63 -8
- package/dist/tools/sports/tools/match.js +44 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,12 @@ Forces the AI to think before acting using a strict block-based flow:
|
|
|
17
17
|
- **Auto-Backup:** Automatically creates `.bak` backups before modifying any file via `deep_code_edit`.
|
|
18
18
|
- **Destructive Action Protection:** Prevents accidental data loss during code refactoring.
|
|
19
19
|
|
|
20
|
+
### ⚽ Sports Intelligence (Upgraded)
|
|
21
|
+
- **Dual-Source Deep Dive:** Simultaneously scrapes news (BBC, Sky) and stats (Understat, FBref) for comprehensive coverage.
|
|
22
|
+
- **Smart xG Extraction:** "Smart Extractor" technology specifically for Understat to get accurate Expected Goals data.
|
|
23
|
+
- **Handicap Odds:** Dedicated search protocols for Asian Handicap and betting market analysis.
|
|
24
|
+
- **Fallback Protection:** Intelligent warning system when API keys are missing.
|
|
25
|
+
|
|
20
26
|
### 🌐 Web Search Integration
|
|
21
27
|
- Built-in support for **Exa**, **Brave**, and **Google Search**.
|
|
22
28
|
- Allows the AI to "pause and research" during the thinking process.
|
|
@@ -31,7 +31,8 @@ export declare const CACHE_CONFIG: {
|
|
|
31
31
|
readonly MAX_SIZE: 1000;
|
|
32
32
|
};
|
|
33
33
|
export declare const SCRAPER_CONFIG: {
|
|
34
|
-
readonly PRIORITY_DOMAINS: readonly ["
|
|
34
|
+
readonly PRIORITY_DOMAINS: readonly ["understat.com", "bbc.co.uk/sport", "sportsmole.co.uk", "skysports.com", "goal.com", "thesquareball.net", "fbref.com", "whoscored.com", "sofascore.com", "flashscore.com"];
|
|
35
|
+
readonly NEWS_DOMAINS: readonly ["bbc.co.uk/sport", "skysports.com", "sportsmole.co.uk", "standard.co.uk/sport", "goal.com", "espn.co.uk/football", "talksport.com", "mirror.co.uk/sport", "dailymail.co.uk/sport"];
|
|
35
36
|
readonly TIMEOUT: 15000;
|
|
36
37
|
readonly USER_AGENT: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
37
38
|
readonly MAX_CONTENT_LENGTH: 50000;
|
|
@@ -44,16 +44,28 @@ export const CACHE_CONFIG = {
|
|
|
44
44
|
export const SCRAPER_CONFIG = {
|
|
45
45
|
// Priority domains for sports data
|
|
46
46
|
PRIORITY_DOMAINS: [
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
47
|
+
'understat.com', // High accuracy (Smart Extractor)
|
|
48
|
+
'bbc.co.uk/sport', // Text-heavy, reliable
|
|
49
|
+
'sportsmole.co.uk', // Text-heavy, reliable
|
|
50
|
+
'skysports.com', // Good text/stats mix
|
|
51
|
+
'goal.com', // Text-heavy
|
|
52
|
+
'thesquareball.net', // Text-heavy
|
|
53
|
+
'fbref.com', // Structured data
|
|
54
|
+
'whoscored.com', // Hard to scrape (JS heavy)
|
|
55
|
+
'sofascore.com', // Hard to scrape (JS heavy)
|
|
56
|
+
'flashscore.com', // Hard to scrape (JS heavy)
|
|
57
|
+
],
|
|
58
|
+
// News-focused domains (for lineups, injuries)
|
|
59
|
+
NEWS_DOMAINS: [
|
|
52
60
|
'bbc.co.uk/sport',
|
|
53
61
|
'skysports.com',
|
|
54
62
|
'sportsmole.co.uk',
|
|
63
|
+
'standard.co.uk/sport',
|
|
55
64
|
'goal.com',
|
|
56
|
-
'
|
|
65
|
+
'espn.co.uk/football',
|
|
66
|
+
'talksport.com',
|
|
67
|
+
'mirror.co.uk/sport',
|
|
68
|
+
'dailymail.co.uk/sport'
|
|
57
69
|
],
|
|
58
70
|
// Scraping timeout in milliseconds
|
|
59
71
|
TIMEOUT: 15000,
|
|
@@ -20,7 +20,8 @@ export declare class ScraperProvider extends ScraperProviderBase {
|
|
|
20
20
|
*/
|
|
21
21
|
protected scrape<T>(url: string, extractor?: (html: string) => T): Promise<APIResponse<T>>;
|
|
22
22
|
/**
|
|
23
|
-
* Specialized extractor for Understat
|
|
23
|
+
* Specialized extractor for Understat
|
|
24
|
+
* Extracts xG, shots, and other advanced metrics from the JSON data embedded in script tags.
|
|
24
25
|
*/
|
|
25
26
|
private extractUnderstatData;
|
|
26
27
|
/**
|
|
@@ -72,3 +73,7 @@ export declare function scrapeMatchContent(url: string): Promise<APIResponse<str
|
|
|
72
73
|
* Find best URL for match data from search results
|
|
73
74
|
*/
|
|
74
75
|
export declare function findBestMatchUrl(urls: string[]): string | null;
|
|
76
|
+
/**
|
|
77
|
+
* Find best URL for news/lineups from search results
|
|
78
|
+
*/
|
|
79
|
+
export declare function findBestNewsUrl(urls: string[]): string | null;
|
|
@@ -85,19 +85,62 @@ export class ScraperProvider extends ScraperProviderBase {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
|
-
* Specialized extractor for Understat
|
|
88
|
+
* Specialized extractor for Understat
|
|
89
|
+
* Extracts xG, shots, and other advanced metrics from the JSON data embedded in script tags.
|
|
89
90
|
*/
|
|
90
91
|
extractUnderstatData(html) {
|
|
91
92
|
const dom = new JSDOM(html);
|
|
92
93
|
const title = dom.window.document.title;
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
let output = `## Understat Analysis: ${title}\n\n`;
|
|
95
|
+
// Helper to extract JSON from script variables
|
|
96
|
+
const extractJson = (varName) => {
|
|
97
|
+
const scripts = Array.from(dom.window.document.querySelectorAll('script'));
|
|
98
|
+
for (const script of scripts) {
|
|
99
|
+
const content = script.textContent || '';
|
|
100
|
+
const regex = new RegExp(`var ${varName}\\s*=\\s*JSON\\.parse\\('([^']+)'\\)`);
|
|
101
|
+
const match = content.match(regex);
|
|
102
|
+
if (match && match[1]) {
|
|
103
|
+
try {
|
|
104
|
+
// Decode hex sequences if any (Understat sometimes uses hex encoding)
|
|
105
|
+
const jsonString = match[1].replace(/\\x([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
106
|
+
return JSON.parse(jsonString);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
const matchData = extractJson('match_info');
|
|
116
|
+
const rosterData = extractJson('rostersData');
|
|
117
|
+
if (matchData) {
|
|
118
|
+
output += `### Match Overview\n`;
|
|
119
|
+
output += `**Team:** ${matchData.h_team} vs ${matchData.a_team}\n`;
|
|
120
|
+
output += `**Score:** ${matchData.h_goals} - ${matchData.a_goals}\n`;
|
|
121
|
+
output += `**xG:** ${matchData.h_xg} - ${matchData.a_xg}\n`;
|
|
122
|
+
output += `**Probabilities:** Home Win: ${matchData.h_win}, Draw: ${matchData.draw}, Away Win: ${matchData.a_win}\n\n`;
|
|
99
123
|
}
|
|
100
|
-
|
|
124
|
+
if (rosterData) {
|
|
125
|
+
output += `### Key Player Stats (xG > 0.1)\n`;
|
|
126
|
+
const processPlayers = (players, teamName) => {
|
|
127
|
+
output += `**${teamName}:**\n`;
|
|
128
|
+
Object.values(players).forEach((p) => {
|
|
129
|
+
if (parseFloat(p.xG) > 0.1) {
|
|
130
|
+
output += `- ${p.player}: xG ${p.xG}, xA ${p.xA}, Shots: ${p.shots}\n`;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
output += '\n';
|
|
134
|
+
};
|
|
135
|
+
if (rosterData.h)
|
|
136
|
+
processPlayers(rosterData.h, matchData?.h_team || 'Home');
|
|
137
|
+
if (rosterData.a)
|
|
138
|
+
processPlayers(rosterData.a, matchData?.a_team || 'Away');
|
|
139
|
+
}
|
|
140
|
+
if (!matchData && !rosterData) {
|
|
141
|
+
return `Title: ${title}\n\n(Understat extraction failed - JSON data not found)`;
|
|
142
|
+
}
|
|
143
|
+
return output;
|
|
101
144
|
}
|
|
102
145
|
/**
|
|
103
146
|
* Specialized extractor for WhoScored (Example)
|
|
@@ -216,3 +259,15 @@ export function findBestMatchUrl(urls) {
|
|
|
216
259
|
// Return first URL if no priority match
|
|
217
260
|
return urls[0] || null;
|
|
218
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Find best URL for news/lineups from search results
|
|
264
|
+
*/
|
|
265
|
+
export function findBestNewsUrl(urls) {
|
|
266
|
+
const newsDomains = SCRAPER_CONFIG.NEWS_DOMAINS;
|
|
267
|
+
for (const url of urls) {
|
|
268
|
+
if (newsDomains.some(domain => url.includes(domain))) {
|
|
269
|
+
return url;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { createAPIProvider } from '../providers/api.js';
|
|
7
7
|
import { getSearchProvider } from '../providers/search.js';
|
|
8
|
-
import { scrapeMatchContent, findBestMatchUrl } from '../providers/scraper.js';
|
|
8
|
+
import { scrapeMatchContent, findBestMatchUrl, findBestNewsUrl } from '../providers/scraper.js';
|
|
9
9
|
import { getGlobalCache, CacheService } from '../core/cache.js';
|
|
10
10
|
import { formatMatchesTable, formatScore, formatMatchStatus } from '../utils/formatter.js';
|
|
11
11
|
import { CACHE_CONFIG, LEAGUES } from '../core/constants.js';
|
|
@@ -76,7 +76,7 @@ async function performMatchAnalysis(homeTeam, awayTeam, league, context) {
|
|
|
76
76
|
{ type: 'form', query: `${homeTeam} ${awayTeam} recent form last 5 matches${dateQuery}`, title: 'Recent Form' },
|
|
77
77
|
{ type: 'news', query: `${baseQuery} team news injuries lineups${dateQuery}`, title: 'Team News & Lineups' },
|
|
78
78
|
{ type: 'stats', query: `${baseQuery} xG expected goals stats${dateQuery}`, title: 'Advanced Metrics' },
|
|
79
|
-
{ type: 'odds', query: `${baseQuery} odds
|
|
79
|
+
{ type: 'odds', query: `${baseQuery} Asian Handicap odds prediction today${dateQuery}`, title: 'Latest Handicap Odds' },
|
|
80
80
|
{ type: 'fatigue', query: `${homeTeam} ${awayTeam} days rest fixture congestion${dateQuery}`, title: 'Fatigue & Schedule' },
|
|
81
81
|
{ type: 'setpieces', query: `${baseQuery} set pieces corners aerial duels${dateQuery}`, title: 'Set Pieces' },
|
|
82
82
|
];
|
|
@@ -111,24 +111,45 @@ async function performMatchAnalysis(homeTeam, awayTeam, league, context) {
|
|
|
111
111
|
const batchResults = await Promise.all(batchPromises);
|
|
112
112
|
combinedResults += batchResults.join('\n');
|
|
113
113
|
}
|
|
114
|
-
// Deep dive: Scrape the best
|
|
114
|
+
// Deep dive: Scrape the best URLs (Dual-Source Strategy)
|
|
115
115
|
if (candidateUrls.length > 0) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
const bestStatsUrl = findBestMatchUrl(candidateUrls);
|
|
117
|
+
const bestNewsUrl = findBestNewsUrl(candidateUrls);
|
|
118
|
+
// 1. Scrape Stats Source (Priority)
|
|
119
|
+
if (bestStatsUrl) {
|
|
120
|
+
combinedResults += `\n--- DEEP DIVE ANALYSIS: STATS & DATA (${bestStatsUrl}) ---\n`;
|
|
119
121
|
try {
|
|
120
|
-
const scrapedContent = await scrapeMatchContent(
|
|
122
|
+
const scrapedContent = await scrapeMatchContent(bestStatsUrl);
|
|
121
123
|
if (scrapedContent.success) {
|
|
122
124
|
combinedResults += scrapedContent.data || '';
|
|
123
125
|
}
|
|
124
126
|
else {
|
|
125
|
-
combinedResults += `(
|
|
127
|
+
combinedResults += `(Stats scrape failed: ${scrapedContent.error})`;
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
catch (scrapeError) {
|
|
129
|
-
combinedResults += `(
|
|
131
|
+
combinedResults += `(Stats scrape failed: ${scrapeError instanceof Error ? scrapeError.message : String(scrapeError)})`;
|
|
130
132
|
}
|
|
131
|
-
combinedResults += `\n--- END
|
|
133
|
+
combinedResults += `\n--- END STATS ---\n`;
|
|
134
|
+
}
|
|
135
|
+
// 2. Scrape News Source (if different from stats)
|
|
136
|
+
if (bestNewsUrl && bestNewsUrl !== bestStatsUrl) {
|
|
137
|
+
combinedResults += `\n--- DEEP DIVE ANALYSIS: NEWS & LINEUPS (${bestNewsUrl}) ---\n`;
|
|
138
|
+
try {
|
|
139
|
+
const scrapedContent = await scrapeMatchContent(bestNewsUrl);
|
|
140
|
+
if (scrapedContent.success) {
|
|
141
|
+
// Truncate news to avoid blowing up context, keep first 3000 chars
|
|
142
|
+
const newsContent = scrapedContent.data?.substring(0, 3000) || '';
|
|
143
|
+
combinedResults += newsContent + (scrapedContent.data && scrapedContent.data.length > 3000 ? '\n...(truncated)' : '');
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
combinedResults += `(News scrape failed: ${scrapedContent.error})`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (scrapeError) {
|
|
150
|
+
combinedResults += `(News scrape failed: ${scrapeError instanceof Error ? scrapeError.message : String(scrapeError)})`;
|
|
151
|
+
}
|
|
152
|
+
combinedResults += `\n--- END NEWS ---\n`;
|
|
132
153
|
}
|
|
133
154
|
}
|
|
134
155
|
combinedResults += `\n--- END DATA ---\n\n`;
|
|
@@ -161,19 +182,16 @@ Market Intelligence, Referee stats, Weather, Fatigue analysis, Set Pieces.`, {
|
|
|
161
182
|
}, async ({ homeTeam, awayTeam, league, context, useApi }) => {
|
|
162
183
|
const errors = [];
|
|
163
184
|
let analysisResult = null;
|
|
185
|
+
// Check API availability explicitly
|
|
186
|
+
const api = createAPIProvider();
|
|
187
|
+
const apiAvailable = api.getStatus().available;
|
|
188
|
+
const forcedFallback = useApi && !apiAvailable;
|
|
164
189
|
// Try API providers first if requested
|
|
165
|
-
if (useApi) {
|
|
190
|
+
if (useApi && apiAvailable) {
|
|
166
191
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Could try to fetch match data from API here
|
|
171
|
-
// For now, we'll use the search-based analysis as fallback
|
|
172
|
-
analysisResult = await performMatchAnalysis(homeTeam, awayTeam, league, context);
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
errors.push('API providers not configured');
|
|
176
|
-
}
|
|
192
|
+
// Could try to fetch match data from API here
|
|
193
|
+
// For now, we'll use the search-based analysis as fallback
|
|
194
|
+
analysisResult = await performMatchAnalysis(homeTeam, awayTeam, league, context);
|
|
177
195
|
}
|
|
178
196
|
catch (error) {
|
|
179
197
|
errors.push(`API error: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -183,6 +201,11 @@ Market Intelligence, Referee stats, Weather, Fatigue analysis, Set Pieces.`, {
|
|
|
183
201
|
if (!analysisResult) {
|
|
184
202
|
analysisResult = await performMatchAnalysis(homeTeam, awayTeam, league, context);
|
|
185
203
|
}
|
|
204
|
+
// Add warning if running in forced fallback mode
|
|
205
|
+
if (forcedFallback && analysisResult) {
|
|
206
|
+
const warning = "⚠️ **WARNING: Fallback Mode Active**\nNo API keys configured. Using web search and scraping (accuracy may vary).\n\n";
|
|
207
|
+
analysisResult = warning + analysisResult;
|
|
208
|
+
}
|
|
186
209
|
return {
|
|
187
210
|
content: [{ type: 'text', text: analysisResult }],
|
|
188
211
|
};
|