fpl-mcp-server 0.1.5__py3-none-any.whl → 0.1.7__py3-none-any.whl
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.
- {fpl_mcp_server-0.1.5.dist-info → fpl_mcp_server-0.1.7.dist-info}/METADATA +67 -58
- fpl_mcp_server-0.1.7.dist-info/RECORD +35 -0
- src/client.py +31 -14
- src/constants.py +4 -88
- src/models.py +15 -30
- src/prompts/__init__.py +2 -0
- src/prompts/captain_recommendation.py +149 -0
- src/prompts/league_analysis.py +12 -5
- src/prompts/player_analysis.py +2 -3
- src/prompts/squad_analysis.py +1 -1
- src/prompts/team_analysis.py +2 -1
- src/prompts/team_selection.py +105 -0
- src/prompts/transfers.py +4 -3
- src/resources/bootstrap.py +7 -1
- src/state.py +10 -4
- src/tools/__init__.py +0 -2
- src/tools/fixtures.py +194 -14
- src/tools/gameweeks.py +0 -198
- src/tools/leagues.py +365 -14
- src/tools/players.py +256 -295
- src/tools/teams.py +1 -189
- src/tools/transfers.py +248 -126
- fpl_mcp_server-0.1.5.dist-info/RECORD +0 -33
- {fpl_mcp_server-0.1.5.dist-info → fpl_mcp_server-0.1.7.dist-info}/WHEEL +0 -0
- {fpl_mcp_server-0.1.5.dist-info → fpl_mcp_server-0.1.7.dist-info}/entry_points.txt +0 -0
- {fpl_mcp_server-0.1.5.dist-info → fpl_mcp_server-0.1.7.dist-info}/licenses/LICENSE +0 -0
src/prompts/league_analysis.py
CHANGED
|
@@ -110,21 +110,28 @@ Step 1: Get league standings to find manager names and team IDs:
|
|
|
110
110
|
Step 2: Compare managers using one of these approaches:
|
|
111
111
|
|
|
112
112
|
**Option A - Individual manager analysis:**
|
|
113
|
-
- Tool: `
|
|
113
|
+
- Tool: `fpl_get_manager_by_team_id`
|
|
114
114
|
- Parameters:
|
|
115
|
-
-
|
|
116
|
-
- league_id: {league_id}
|
|
115
|
+
- team_id: [Team ID] (found in standings)
|
|
117
116
|
- gameweek: {gameweek}
|
|
118
117
|
- Returns: Detailed team sheet with starting XI, bench, captain, transfers, points
|
|
119
118
|
|
|
120
|
-
**Option B - Side-by-side comparison:**
|
|
119
|
+
**Option B - Side-by-side comparison (General):**
|
|
121
120
|
- Tool: `fpl_compare_managers`
|
|
122
121
|
- Parameters:
|
|
123
|
-
- manager_names: ["Manager1", "Manager2"
|
|
122
|
+
- manager_names: ["Manager1", "Manager2"]
|
|
124
123
|
- league_id: {league_id}
|
|
125
124
|
- gameweek: {gameweek}
|
|
126
125
|
- Returns: Comparison with common players, differentials, captain choices
|
|
127
126
|
|
|
127
|
+
**Option C - Deep Rival Analysis (Head-to-Head):**
|
|
128
|
+
- Tool: `fpl_analyze_rival`
|
|
129
|
+
- Parameters:
|
|
130
|
+
- my_team_id: [Your Team ID]
|
|
131
|
+
- rival_team_id: [Rival Team ID]
|
|
132
|
+
- gameweek: {gameweek}
|
|
133
|
+
- Returns: Comprehensive stats, differentials, and threat assessment
|
|
134
|
+
|
|
128
135
|
**Additional data sources:**
|
|
129
136
|
- Resource `fpl://bootstrap/players` - All player details, ownership %, positions, prices
|
|
130
137
|
- Resource `fpl://current-gameweek` - Current gameweek status and deadline information
|
src/prompts/player_analysis.py
CHANGED
|
@@ -123,12 +123,11 @@ Present side-by-side:
|
|
|
123
123
|
|
|
124
124
|
## 🔧 **Tool Calls**
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
1. **`fpl://player/{{{{player_name}}}}/summary`** → Get comprehensive stats, fixtures, history
|
|
126
|
+
1. **`fpl_compare_players(player_names=[p1, p2, ...])`** → Get comprehensive stats, fixtures, history side-by-side
|
|
128
127
|
*Provides: xG, xA, xGI, minutes, goals, assists, upcoming fixtures*
|
|
129
128
|
2. **`fpl://bootstrap/players`** → Get ownership %, price, transfer trends
|
|
130
129
|
*Provides: selected_by_percent, now_cost, transfers_in/out_event*
|
|
131
|
-
3. **`fpl_get_top_performers
|
|
130
|
+
3. **`fpl_get_top_performers(num_gameweeks=5)`** → Benchmark against top xGI players
|
|
132
131
|
|
|
133
132
|
---
|
|
134
133
|
|
src/prompts/squad_analysis.py
CHANGED
|
@@ -119,7 +119,7 @@ Instead of arbitrary PPG thresholds, use xGI/90:
|
|
|
119
119
|
## 🔧 **Tool Calls**
|
|
120
120
|
|
|
121
121
|
Use these tools and resources:
|
|
122
|
-
1. `
|
|
122
|
+
1. `fpl_get_manager_by_team_id(team_id={team_id})` → Current squad composition
|
|
123
123
|
2. `fpl_get_top_performers(num_gameweeks={num_gameweeks})` → Benchmark against top xGI players
|
|
124
124
|
3. For each player:
|
|
125
125
|
- `fpl://player/{{{{player_name}}}}/summary` → xG, xA, xGI, minutes, fixtures
|
src/prompts/team_analysis.py
CHANGED
|
@@ -108,7 +108,8 @@ Calculate **rolling 3-GW average FDR**:
|
|
|
108
108
|
|
|
109
109
|
## 🔧 **Tool Calls**
|
|
110
110
|
|
|
111
|
-
Use: `
|
|
111
|
+
Use: `fpl_analyze_team_fixtures(team_name="{team_name}", num_gameweeks={num_gameweeks})`
|
|
112
|
+
For broader analysis (finding ANY team with good fixtures), use `fpl_find_fixture_opportunities`.
|
|
112
113
|
Enrich with: `fpl://bootstrap/teams` for opponent strength proxy (if xGC not available, use team strength rank)
|
|
113
114
|
|
|
114
115
|
---
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FPL MCP Prompts - Team Selection.
|
|
3
|
+
|
|
4
|
+
Prompts guide the LLM in selecting the optimal starting XI and bench ordering
|
|
5
|
+
for a specific gameweek.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ..tools import mcp
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.prompt()
|
|
12
|
+
def select_team(team_id: int, gameweek: int | None = None) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Optimize Starting XI and Bench using fixture analysis and player status.
|
|
15
|
+
|
|
16
|
+
This prompt guides the LLM to choose the best starting lineup and bench order
|
|
17
|
+
based on fixture difficulty, player availability, and form.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
team_id: Manager's FPL team ID
|
|
21
|
+
gameweek: Target gameweek (defaults to current/next if None)
|
|
22
|
+
"""
|
|
23
|
+
gameweek_text = f"gameweek {gameweek}" if gameweek else "the upcoming gameweek"
|
|
24
|
+
gameweek_display = f"{gameweek}" if gameweek else "Upcoming"
|
|
25
|
+
|
|
26
|
+
return f"""Optimize the Starting XI and Bench for team ID {team_id} in {gameweek_text}.
|
|
27
|
+
**OBJECTIVE: Select the highest-scoring Starting XI and optimize Bench ordering.**
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚦 **Workflow & Logic**
|
|
32
|
+
|
|
33
|
+
1. **Get Squad & Status**: Fetch manager's team and check for injuries/suspensions.
|
|
34
|
+
2. **Analyze Fixtures**: Evaluate opponent strength (Attack vs Defense).
|
|
35
|
+
3. **Select Lineup**: Best 11 players regardless of formation (valid formations only: 3-4-3, 3-5-2, 4-4-2, 4-3-3, 5-3-2, etc.).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🧠 **Selection Strategy**
|
|
40
|
+
|
|
41
|
+
### **1. Starting XI Priority (Must Starts)**
|
|
42
|
+
* **Premiums**: Always start (e.g., Salah, Haaland) unless injured.
|
|
43
|
+
* **Form Attackers**: Start players with xGI > 0.5 recently, even with tricky fixtures (Attack beats Defense).
|
|
44
|
+
* **Defenders with Clean Sheet Potential**: Start defenders vs Bottom 5 Attacks.
|
|
45
|
+
* **Attacking Defenders**: Start defenders with high xA (e.g., Trent, Porro) regardless of fixture, unless playing Man City/Arsenal away.
|
|
46
|
+
|
|
47
|
+
### **2. Bench Decisions (The "Dilemma" Area)**
|
|
48
|
+
* **Bench Defenders vs Top 6 Attack**: If you have a decent backup mid/fwd, bench the defender playing a top team.
|
|
49
|
+
* **Bench Rotation Risks**: If a player is a massive rotation risk (e.g., Pep Roulette), they can still start if the ceiling is high, but have a secure #1 bench sub ready.
|
|
50
|
+
* **Bench Injured/Suspended**: Move to slots #2 and #3.
|
|
51
|
+
|
|
52
|
+
### **3. Optimizing Bench Order**
|
|
53
|
+
* **Slot 1**: **Highest Ceiling**. The player who can score 10+ points if they come on (e.g., Explosive Winger vs tough defense > 2pt Defender).
|
|
54
|
+
* **Slot 2**: **Safety**. The 90-min defender who guarantees 1-2 points if Slot 1 doesn't play.
|
|
55
|
+
* **Slot 3**: **Fodder/Red Flags**.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 📝 **Output Format:**
|
|
60
|
+
|
|
61
|
+
**Recommended Lineup for Gameweek {gameweek_display}**
|
|
62
|
+
|
|
63
|
+
*(Formation: [e.g. 3-4-3])*
|
|
64
|
+
|
|
65
|
+
**Defense**
|
|
66
|
+
* **GK**: [Name] (vs [Opponent])
|
|
67
|
+
* *Rationale*: [One line reason, e.g., "Opponent lowest xG in league"]
|
|
68
|
+
* **DEF**: [Name] (vs [Opponent])
|
|
69
|
+
* **DEF**: [Name] (vs [Opponent])
|
|
70
|
+
* ...
|
|
71
|
+
|
|
72
|
+
**Midfield**
|
|
73
|
+
* **MID**: [Name] (vs [Opponent])
|
|
74
|
+
* **MID**: [Name] (vs [Opponent])
|
|
75
|
+
* ...
|
|
76
|
+
|
|
77
|
+
**Forwards**
|
|
78
|
+
* **FWD**: [Name] (vs [Opponent])
|
|
79
|
+
* ...
|
|
80
|
+
|
|
81
|
+
**©️ Captain**: [Name] (Run `recommend_captain` for detailed analysis)
|
|
82
|
+
**⚡ Vice-Captain**: [Name] (Secure starter with highest ceiling)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
**Bench (Critical Order)**
|
|
87
|
+
1. **[Name]** ([Pos] vs [Opponent])
|
|
88
|
+
* *Why #1?*: [e.g., "High ceiling upside despite tough fixture"]
|
|
89
|
+
2. **[Name]** ([Pos] vs [Opponent])
|
|
90
|
+
3. **[Name]** ([Pos] vs [Opponent])
|
|
91
|
+
4. **GK [Name]** ([Pos] vs [Opponent])
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## ⚠️ **Transfer Alert (Optional)**
|
|
96
|
+
* If the team has **>2 non-playing players** (Red flags/Bench fodder) in the starting XI/Bench 1:
|
|
97
|
+
* **Recommendation**: "Consider a transfer for [Player Name] → [Replacement Tool]"
|
|
98
|
+
|
|
99
|
+
## 🔧 **Tool Usage**
|
|
100
|
+
1. `fpl_get_manager_by_team_id(team_id={team_id})` → Get squad.
|
|
101
|
+
2. `fpl_get_gameweek_fixtures(gameweek={gameweek})` → Get matchups.
|
|
102
|
+
3. `fpl_get_player_summary(player_id=...)` -> Check status if flagged.
|
|
103
|
+
|
|
104
|
+
**Begin Selection Analysis.**
|
|
105
|
+
"""
|
src/prompts/transfers.py
CHANGED
|
@@ -150,11 +150,12 @@ Using `fpl_get_top_performers(num_gameweeks=5)`:
|
|
|
150
150
|
|
|
151
151
|
## 🔧 **Tool Calls**
|
|
152
152
|
|
|
153
|
-
1. `
|
|
153
|
+
1. `fpl_get_manager_by_team_id(team_id={team_id})` → Current squad with transfer context
|
|
154
154
|
2. `fpl_get_top_performers(num_gameweeks=5)` → Find high xGI players for replacements
|
|
155
|
-
3.
|
|
155
|
+
3. Use `fpl_analyze_transfer(player_out=..., player_in=...)` to validate your top priority move.
|
|
156
|
+
4. For other candidates:
|
|
156
157
|
- `fpl://player/{{{{name}}}}/summary` → xG, xA, fixtures, status
|
|
157
|
-
|
|
158
|
+
5. `fpl://bootstrap/players` → Price, ownership, transfer trends
|
|
158
159
|
|
|
159
160
|
---
|
|
160
161
|
|
src/resources/bootstrap.py
CHANGED
|
@@ -7,6 +7,7 @@ Bootstrap resources expose static FPL data that rarely changes during a season.
|
|
|
7
7
|
from datetime import UTC, datetime
|
|
8
8
|
|
|
9
9
|
from ..client import FPLClient
|
|
10
|
+
from ..constants import PlayerPosition
|
|
10
11
|
from ..state import store
|
|
11
12
|
from ..tools import mcp
|
|
12
13
|
|
|
@@ -31,7 +32,12 @@ async def get_all_players_resource() -> str:
|
|
|
31
32
|
output = [f"**All FPL Players ({len(players)} total)**\n"]
|
|
32
33
|
|
|
33
34
|
# Group by position
|
|
34
|
-
positions = {
|
|
35
|
+
positions = {
|
|
36
|
+
PlayerPosition.GOALKEEPER.value: [],
|
|
37
|
+
PlayerPosition.DEFENDER.value: [],
|
|
38
|
+
PlayerPosition.MIDFIELDER.value: [],
|
|
39
|
+
PlayerPosition.FORWARD.value: [],
|
|
40
|
+
}
|
|
35
41
|
for p in players:
|
|
36
42
|
if p.position in positions:
|
|
37
43
|
positions[p.position].append(p)
|
src/state.py
CHANGED
|
@@ -4,6 +4,12 @@ import logging
|
|
|
4
4
|
from .cache import cache_manager
|
|
5
5
|
from .client import FPLClient
|
|
6
6
|
from .config import settings
|
|
7
|
+
from .constants import (
|
|
8
|
+
FUZZY_MATCH_PENALTY,
|
|
9
|
+
FUZZY_MATCH_THRESHOLD,
|
|
10
|
+
PERFECT_MATCH_SCORE,
|
|
11
|
+
SUBSTRING_MATCH_PENALTY,
|
|
12
|
+
)
|
|
7
13
|
from .models import BootstrapData, ElementData, EventData, FixtureData
|
|
8
14
|
|
|
9
15
|
logger = logging.getLogger("fpl_state")
|
|
@@ -193,7 +199,7 @@ class SessionStore:
|
|
|
193
199
|
# 1. Exact match
|
|
194
200
|
if normalized_query in self.player_name_map:
|
|
195
201
|
for player_id in self.player_name_map[normalized_query]:
|
|
196
|
-
results[player_id] =
|
|
202
|
+
results[player_id] = PERFECT_MATCH_SCORE
|
|
197
203
|
|
|
198
204
|
# 2. Substring match (contains)
|
|
199
205
|
if not results:
|
|
@@ -205,16 +211,16 @@ class SessionStore:
|
|
|
205
211
|
)
|
|
206
212
|
for player_id in player_ids:
|
|
207
213
|
if player_id not in results or similarity > results[player_id]:
|
|
208
|
-
results[player_id] = similarity *
|
|
214
|
+
results[player_id] = similarity * SUBSTRING_MATCH_PENALTY
|
|
209
215
|
|
|
210
216
|
# 3. Fuzzy matching (if enabled and no good matches yet)
|
|
211
217
|
if fuzzy and (not results or max(results.values()) < 0.7):
|
|
212
218
|
for name_key, player_ids in self.player_name_map.items():
|
|
213
219
|
similarity = SequenceMatcher(None, normalized_query, name_key).ratio()
|
|
214
|
-
if similarity >=
|
|
220
|
+
if similarity >= FUZZY_MATCH_THRESHOLD:
|
|
215
221
|
for player_id in player_ids:
|
|
216
222
|
if player_id not in results or similarity > results[player_id]:
|
|
217
|
-
results[player_id] = similarity *
|
|
223
|
+
results[player_id] = similarity * FUZZY_MATCH_PENALTY
|
|
218
224
|
|
|
219
225
|
# Convert to list of tuples and sort by score
|
|
220
226
|
player_matches = [
|
src/tools/__init__.py
CHANGED
|
@@ -6,8 +6,6 @@ from mcp.server.fastmcp import FastMCP
|
|
|
6
6
|
# Create shared MCP instance following Python naming convention: {service}_mcp
|
|
7
7
|
mcp = FastMCP("fpl_mcp")
|
|
8
8
|
|
|
9
|
-
# Import all tool modules (this registers tools with mcp) # noqa: E402
|
|
10
|
-
# Import resources and prompts (this registers them with mcp)
|
|
11
9
|
from .. import (
|
|
12
10
|
prompts, # noqa: F401
|
|
13
11
|
resources, # noqa: F401
|
src/tools/fixtures.py
CHANGED
|
@@ -11,14 +11,8 @@ from ..utils import (
|
|
|
11
11
|
format_json_response,
|
|
12
12
|
handle_api_error,
|
|
13
13
|
)
|
|
14
|
-
|
|
15
|
-
# Import shared mcp instance
|
|
16
14
|
from . import mcp
|
|
17
15
|
|
|
18
|
-
# =============================================================================
|
|
19
|
-
# Pydantic Input Models
|
|
20
|
-
# =============================================================================
|
|
21
|
-
|
|
22
16
|
|
|
23
17
|
class GetFixturesForGameweekInput(BaseModel):
|
|
24
18
|
"""Input model for getting fixtures for a gameweek."""
|
|
@@ -34,9 +28,25 @@ class GetFixturesForGameweekInput(BaseModel):
|
|
|
34
28
|
)
|
|
35
29
|
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
31
|
+
class FindFixtureOpportunitiesInput(BaseModel):
|
|
32
|
+
"""Input model for finding fixture opportunities."""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
35
|
+
|
|
36
|
+
num_gameweeks: int = Field(
|
|
37
|
+
default=5, description="Number of future gameweeks to analyze (default: 5)", ge=3, le=10
|
|
38
|
+
)
|
|
39
|
+
max_teams: int = Field(
|
|
40
|
+
default=3, description="Number of top teams to return (default: 3)", ge=1, le=5
|
|
41
|
+
)
|
|
42
|
+
positions: list[str] | None = Field(
|
|
43
|
+
default=None,
|
|
44
|
+
description="Filter recommended players by position (e.g. ['Midfielder', 'Forward'])",
|
|
45
|
+
)
|
|
46
|
+
response_format: ResponseFormat = Field(
|
|
47
|
+
default=ResponseFormat.MARKDOWN,
|
|
48
|
+
description="Output format: 'markdown' for human-readable or 'json' for machine-readable",
|
|
49
|
+
)
|
|
40
50
|
|
|
41
51
|
|
|
42
52
|
async def _create_client():
|
|
@@ -47,11 +57,6 @@ async def _create_client():
|
|
|
47
57
|
return client
|
|
48
58
|
|
|
49
59
|
|
|
50
|
-
# =============================================================================
|
|
51
|
-
# MCP Tools
|
|
52
|
-
# =============================================================================
|
|
53
|
-
|
|
54
|
-
|
|
55
60
|
@mcp.tool(
|
|
56
61
|
name="fpl_get_fixtures_for_gameweek",
|
|
57
62
|
annotations={
|
|
@@ -160,3 +165,178 @@ async def fpl_get_fixtures_for_gameweek(params: GetFixturesForGameweekInput) ->
|
|
|
160
165
|
|
|
161
166
|
except Exception as e:
|
|
162
167
|
return handle_api_error(e)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@mcp.tool(
|
|
171
|
+
name="fpl_find_fixture_opportunities",
|
|
172
|
+
annotations={
|
|
173
|
+
"title": "Find Fixture Opportunities",
|
|
174
|
+
"readOnlyHint": True,
|
|
175
|
+
"destructiveHint": False,
|
|
176
|
+
"idempotentHint": True,
|
|
177
|
+
"openWorldHint": True,
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
async def fpl_find_fixture_opportunities(params: FindFixtureOpportunitiesInput) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Find teams with the easiest upcoming fixtures and their best assets.
|
|
183
|
+
|
|
184
|
+
Analyzes fixture difficulty for all 20 teams over the next N gameweeks.
|
|
185
|
+
Identifies teams with the most favorable schedule and recommends their
|
|
186
|
+
top-performing players (filtered by position if requested).
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
params (FindFixtureOpportunitiesInput): Validated input parameters containing:
|
|
190
|
+
- num_gameweeks (int): Number of gameweeks to analyze (3-10)
|
|
191
|
+
- max_teams (int): Number of teams to recommend (1-5)
|
|
192
|
+
- positions (list[str] | None): Optional position filter
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
str: Analysis of best teams to target and their key players
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
- Target next 5 GWs: num_gameweeks=5
|
|
199
|
+
- Find best attackers: positions=['Midfielder', 'Forward']
|
|
200
|
+
|
|
201
|
+
Error Handling:
|
|
202
|
+
- Returns error if data unavailable
|
|
203
|
+
- Returns formatted error message if API fails
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
await _create_client()
|
|
207
|
+
if not store.bootstrap_data:
|
|
208
|
+
return "Error: Player data not available."
|
|
209
|
+
|
|
210
|
+
# Determine current gameweek
|
|
211
|
+
current_gw_data = store.get_current_gameweek()
|
|
212
|
+
current_gw = current_gw_data.id if current_gw_data else 1
|
|
213
|
+
start_gw = current_gw + 1
|
|
214
|
+
|
|
215
|
+
# Calculate average difficulty for each team
|
|
216
|
+
team_difficulties = []
|
|
217
|
+
|
|
218
|
+
# Helper to get difficulty for a team ID in a GW
|
|
219
|
+
def get_team_fixtures(team_id):
|
|
220
|
+
fixtures = []
|
|
221
|
+
for gw in range(start_gw, start_gw + params.num_gameweeks):
|
|
222
|
+
if gw > 38:
|
|
223
|
+
break
|
|
224
|
+
# Find fixture for this team in this GW
|
|
225
|
+
# Use enriched fixtures if pre-calculated, or search raw
|
|
226
|
+
# Searching raw is faster here than full enrich loop
|
|
227
|
+
matches = [
|
|
228
|
+
f
|
|
229
|
+
for f in store.fixtures_data
|
|
230
|
+
if f.event == gw and (f.team_h == team_id or f.team_a == team_id)
|
|
231
|
+
]
|
|
232
|
+
for m in matches:
|
|
233
|
+
is_home = m.team_h == team_id
|
|
234
|
+
diff = m.team_h_difficulty if is_home else m.team_a_difficulty
|
|
235
|
+
opponent_id = m.team_a if is_home else m.team_h
|
|
236
|
+
opponent = next(
|
|
237
|
+
(t for t in store.bootstrap_data.teams if t.id == opponent_id), None
|
|
238
|
+
)
|
|
239
|
+
fixtures.append(
|
|
240
|
+
{
|
|
241
|
+
"gameweek": gw,
|
|
242
|
+
"difficulty": diff,
|
|
243
|
+
"opponent": opponent.short_name if opponent else "UNK",
|
|
244
|
+
"is_home": is_home,
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
return fixtures
|
|
248
|
+
|
|
249
|
+
for team in store.bootstrap_data.teams:
|
|
250
|
+
fixtures = get_team_fixtures(team.id)
|
|
251
|
+
if not fixtures:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
avg_diff = sum(f["difficulty"] for f in fixtures) / len(fixtures)
|
|
255
|
+
team_difficulties.append({"team": team, "avg_diff": avg_diff, "fixtures": fixtures})
|
|
256
|
+
|
|
257
|
+
# Sort by easiest (lowest avg difficulty)
|
|
258
|
+
team_difficulties.sort(key=lambda x: x["avg_diff"])
|
|
259
|
+
top_teams = team_difficulties[: params.max_teams]
|
|
260
|
+
|
|
261
|
+
# Find top players for these teams
|
|
262
|
+
# Map position string to element_type (1=GKP, 2=DEF, 3=MID, 4=FWD)
|
|
263
|
+
pos_map = {"Goalkeeper": 1, "Defender": 2, "Midfielder": 3, "Forward": 4}
|
|
264
|
+
target_types = []
|
|
265
|
+
if params.positions:
|
|
266
|
+
for p in params.positions:
|
|
267
|
+
p_norm = p.capitalize()
|
|
268
|
+
# Handle plurals
|
|
269
|
+
if p_norm.endswith("s"):
|
|
270
|
+
p_norm = p_norm[:-1]
|
|
271
|
+
idx = pos_map.get(p_norm)
|
|
272
|
+
if idx:
|
|
273
|
+
target_types.append(idx)
|
|
274
|
+
|
|
275
|
+
result_teams = []
|
|
276
|
+
|
|
277
|
+
for item in top_teams:
|
|
278
|
+
team = item["team"]
|
|
279
|
+
# Get players for this team
|
|
280
|
+
team_players = [
|
|
281
|
+
p for p in store.bootstrap_data.elements if p.team == team.id and p.status != "u"
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
if target_types:
|
|
285
|
+
team_players = [p for p in team_players if p.element_type in target_types]
|
|
286
|
+
|
|
287
|
+
# Sort by form (best assets)
|
|
288
|
+
top_assets = sorted(team_players, key=lambda x: float(x.form), reverse=True)[:3]
|
|
289
|
+
|
|
290
|
+
result_teams.append(
|
|
291
|
+
{
|
|
292
|
+
"team_name": team.name,
|
|
293
|
+
"avg_diff": item["avg_diff"],
|
|
294
|
+
"fixtures": item["fixtures"],
|
|
295
|
+
"best_players": top_assets,
|
|
296
|
+
}
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if params.response_format == ResponseFormat.JSON:
|
|
300
|
+
json_out = {
|
|
301
|
+
"start_gameweek": start_gw,
|
|
302
|
+
"end_gameweek": start_gw + params.num_gameweeks - 1,
|
|
303
|
+
"opportunities": [],
|
|
304
|
+
}
|
|
305
|
+
for rt in result_teams:
|
|
306
|
+
# Format fixtures string
|
|
307
|
+
fixtures_list = [
|
|
308
|
+
f"{f['opponent']} ({'H' if f['is_home'] else 'A'})" for f in rt["fixtures"]
|
|
309
|
+
]
|
|
310
|
+
json_out["opportunities"].append(
|
|
311
|
+
{
|
|
312
|
+
"team": rt["team_name"],
|
|
313
|
+
"difficulty_score": round(rt["avg_diff"], 2),
|
|
314
|
+
"fixtures": fixtures_list,
|
|
315
|
+
"recommended_players": [p.web_name for p in rt["best_players"]],
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
return format_json_response(json_out)
|
|
319
|
+
|
|
320
|
+
# Markdown Output
|
|
321
|
+
output = [
|
|
322
|
+
f"## 🗓️ Fixture Opportunities (Next {params.num_gameweeks} GWs)",
|
|
323
|
+
"Top teams with the easiest schedules to target:",
|
|
324
|
+
"",
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
for i, rt in enumerate(result_teams, 1):
|
|
328
|
+
fixtures_str = " - ".join(
|
|
329
|
+
[f"**{f['opponent']}** ({'H' if f['is_home'] else 'A'})" for f in rt["fixtures"]]
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
output.append(f"### {i}. {rt['team_name']} (Diff: {rt['avg_diff']:.1f})")
|
|
333
|
+
output.append(f"🗓️ **Schedule:** {fixtures_str}")
|
|
334
|
+
|
|
335
|
+
player_names = [f"{p.web_name} ({p.form} form)" for p in rt["best_players"]]
|
|
336
|
+
output.append(f"🔥 **Targets:** {', '.join(player_names)}")
|
|
337
|
+
output.append("")
|
|
338
|
+
|
|
339
|
+
return "\n".join(output)
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
return handle_api_error(e)
|