|
|
import gradio as gr |
|
|
import os |
|
|
import requests |
|
|
import re |
|
|
import traceback |
|
|
from huggingface_hub import InferenceClient |
|
|
|
|
|
|
|
|
TOKEN = os.getenv("HF_TOKEN") |
|
|
if not TOKEN: |
|
|
raise ValueError("HF_TOKEN environment variable is missing. Please add it in Settings.") |
|
|
MODEL_ID = "Qwen/Qwen2.5-Coder-32B-Instruct" |
|
|
|
|
|
client = InferenceClient(MODEL_ID, token=TOKEN) |
|
|
|
|
|
|
|
|
fpl_theme = gr.themes.Base( |
|
|
primary_hue="green", |
|
|
secondary_hue="indigo", |
|
|
neutral_hue="slate", |
|
|
).set( |
|
|
body_background_fill="#37003c", |
|
|
block_background_fill="#47004d", |
|
|
body_text_color="#ffffff", |
|
|
block_label_text_color="#00ff85", |
|
|
block_title_text_color="#00ff85", |
|
|
button_primary_background_fill="#00ff85", |
|
|
button_primary_text_color="#37003c", |
|
|
border_color_primary="#00ff85", |
|
|
) |
|
|
|
|
|
|
|
|
BASE_URL = "https://fantasy.premierleague.com/api" |
|
|
HEADERS = { |
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" |
|
|
} |
|
|
DATA_CACHE = {"bootstrap": None} |
|
|
|
|
|
def fetch_bootstrap(): |
|
|
"""Fetches data and caches it. Retries if empty.""" |
|
|
if DATA_CACHE["bootstrap"] is None: |
|
|
try: |
|
|
r = requests.get(f"{BASE_URL}/bootstrap-static/", headers=HEADERS, timeout=10) |
|
|
if r.status_code == 200: |
|
|
DATA_CACHE["bootstrap"] = r.json() |
|
|
else: |
|
|
print(f"❌ API connection failed. Status: {r.status_code}") |
|
|
return None |
|
|
except Exception as e: |
|
|
print(f"❌ Network error: {e}") |
|
|
return None |
|
|
return DATA_CACHE.get("bootstrap") |
|
|
|
|
|
def get_team_name(team_id): |
|
|
data = fetch_bootstrap() |
|
|
if not data: return "UNK" |
|
|
for t in data['teams']: |
|
|
if t['id'] == team_id: return t['short_name'] |
|
|
return "UNK" |
|
|
|
|
|
def get_player_id(name_query): |
|
|
data = fetch_bootstrap() |
|
|
if not data: return None |
|
|
name_query = str(name_query).lower().strip() |
|
|
for p in data['elements']: |
|
|
full_name = f"{p['first_name']} {p['second_name']}".lower() |
|
|
if name_query in full_name or name_query in p['web_name'].lower(): return p |
|
|
return None |
|
|
|
|
|
def get_player_report(name_query): |
|
|
player = get_player_id(name_query) |
|
|
if not player: return f"❌ Player '{name_query}' not found in 2024/25 Database." |
|
|
try: |
|
|
r = requests.get(f"{BASE_URL}/element-summary/{player['id']}/", headers=HEADERS, timeout=10) |
|
|
if r.status_code != 200: return "⚠️ API Error: Cannot fetch history." |
|
|
|
|
|
data = r.json() |
|
|
history = data.get('history', []) |
|
|
last_3 = history[-3:] if len(history) >= 3 else history |
|
|
recent_pts = sum(m['total_points'] for m in last_3) |
|
|
|
|
|
fixtures = data.get('fixtures', []) |
|
|
next_3 = fixtures[:3] |
|
|
fix_str = "" |
|
|
for f in next_3: |
|
|
is_home = f['is_home'] |
|
|
opp_id = f['team_a'] if is_home else f['team_h'] |
|
|
opp = get_team_name(opp_id) |
|
|
diff = f['difficulty'] |
|
|
fix_str += f"{opp} (Dif:{diff}), " |
|
|
|
|
|
return ( |
|
|
f"### 📊 {player['web_name']}\n" |
|
|
f"**Price:** £{player['now_cost']/10}m | **Form:** {player['form']}\n" |
|
|
f"**Next 3:** {fix_str.strip(', ')}\n" |
|
|
f"**Last 3 Pts:** {recent_pts}" |
|
|
) |
|
|
except: return "⚠️ Error processing stats." |
|
|
|
|
|
def compare_players(name1, name2): |
|
|
p1 = get_player_id(name1) |
|
|
p2 = get_player_id(name2) |
|
|
if not p1 or not p2: return "❌ Player not found." |
|
|
s1 = (float(p1['form']) * 0.6) + (float(p1['points_per_game']) * 0.4) |
|
|
s2 = (float(p2['form']) * 0.6) + (float(p2['points_per_game']) * 0.4) |
|
|
return f"### ⚔️ Compare\n**{p1['web_name']}** ({s1:.1f}) vs **{p2['web_name']}** ({s2:.1f})\nPick: **{p1['web_name'] if s1>s2 else p2['web_name']}**" |
|
|
|
|
|
def find_hidden_gems(position="MID", max_price=15.0): |
|
|
data = fetch_bootstrap() |
|
|
if not data: return "⚠️ API Error: Cannot load database." |
|
|
|
|
|
pos_map = { |
|
|
"GK": 1, "GKP": 1, "GOALKEEPER": 1, |
|
|
"DEF": 2, "DEFENDER": 2, "DF": 2, "CB": 2, |
|
|
"MID": 3, "MIDFIELDER": 3, "MF": 3, "WINGER": 3, |
|
|
"FWD": 4, "FORWARD": 4, "STRIKER": 4, "ATTACKER": 4, "ATT": 4 |
|
|
} |
|
|
pos_key = str(position).upper().strip() |
|
|
pos_id = pos_map.get(pos_key, 3) |
|
|
|
|
|
try: |
|
|
limit = float(max_price) |
|
|
if limit > 200: limit = limit / 1000000 if limit > 100000 else limit / 10 |
|
|
except: limit = 15.0 |
|
|
|
|
|
gems = [] |
|
|
for p in data['elements']: |
|
|
cost = p['now_cost'] / 10 |
|
|
|
|
|
if p['element_type'] == pos_id and cost <= limit: |
|
|
if float(p['form']) > 0.0 or float(p['selected_by_percent']) > 0.5: |
|
|
gems.append(p) |
|
|
|
|
|
|
|
|
gems = sorted(gems, key=lambda x: float(x['form']), reverse=True)[:5] |
|
|
if not gems: return f"No gems found for {pos_key} under £{limit}m." |
|
|
|
|
|
res = f"### 💎 Gems ({pos_key} < £{limit}m)\n" |
|
|
for g in gems: |
|
|
r = requests.get(f"{BASE_URL}/element-summary/{g['id']}/", headers=HEADERS, timeout=5) |
|
|
next_fix = "UNK" |
|
|
if r.status_code == 200: |
|
|
fixtures = r.json().get('fixtures', []) |
|
|
if fixtures: |
|
|
f = fixtures[0] |
|
|
opp = get_team_name(f['team_a'] if f['is_home'] else f['team_h']) |
|
|
next_fix = f"{opp} (Dif:{f['difficulty']})" |
|
|
res += f"- **{g['web_name']}** (£{g['now_cost']/10}m) Form: {g['form']} | Next: {next_fix}\n" |
|
|
return res |
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = """ |
|
|
You are 'FPL Scout', an elite Fantasy Premier League tactical assistant. |
|
|
You have access to a LIVE database of the current 2024/25 season via tools. |
|
|
|
|
|
### 🧠 KNOWLEDGE BASE (OFFICIAL RULES - USE THIS): |
|
|
- **Wildcard:** Allows unlimited PERMANENT transfers. Best used when your team has multiple injuries or poor form. |
|
|
- **Free Hit:** Allows unlimited transfers for ONE Gameweek only. Squad reverts to previous state. Use in Blank/Double Gameweeks. |
|
|
- **Bench Boost:** Points from bench players count for one GW. Use when your bench has strong fixtures. |
|
|
- **Triple Captain:** Captain gets 3x points. Best used on Double Gameweeks. |
|
|
|
|
|
### 🛑 ANTI-HALLUCINATION PROTOCOLS: |
|
|
1. **IGNORE INTERNAL DATA:** Your training data is from the past. Do NOT rely on it for player teams, prices, or form. Jordan Henderson is NOT at Liverpool. |
|
|
2. **TOOL DEPENDENCY:** If a user asks for "Best Midfielder", you CANNOT answer until you run `[TOOL:find_hidden_gems:...]`. |
|
|
3. **STOP SEQUENCE:** After outputting a `[TOOL:...]` tag, STOP generating text immediately. Wait for the data. |
|
|
|
|
|
### 🛠️ AVAILABLE TOOLS: |
|
|
1. [TOOL:get_player_report:Name] |
|
|
- Use for: Specific stats, form, price, and **Upcoming Fixtures**. |
|
|
- Example: "Should I keep Salah?" -> [TOOL:get_player_report:Salah] |
|
|
|
|
|
2. [TOOL:compare_players:Name1,Name2] |
|
|
- Use for: Captaincy choices, "Start vs Bench", or Transfer dilemmas. |
|
|
- Example: "Saka or Palmer?" -> [TOOL:compare_players:Saka,Palmer] |
|
|
|
|
|
3. [TOOL:find_hidden_gems:Position,MaxPrice] |
|
|
- Use for: Differentials, replacements, budget enablers. |
|
|
- Defaults: If user doesn't specify price, use 15.0. |
|
|
- Example: "Cheap defender?" -> [TOOL:find_hidden_gems:DEF,4.5] |
|
|
|
|
|
### 📝 RESPONSE GUIDELINES (After Tool Result): |
|
|
- **Fixtures are King:** Analyze the 'Next 3' fixtures in the tool output. Green/Easy fixtures = BUY. |
|
|
- **Format:** Use **Bold** for names/prices. Use lists. |
|
|
- **Failure Handling:** If the tool returns "No gems found" or "API Error", simply say: "I couldn't find any players matching those criteria in the live database." DO NOT INVENT A LIST. |
|
|
""" |
|
|
|
|
|
def chat_logic(message, history): |
|
|
messages = [{"role": "system", "content": SYSTEM_PROMPT}] |
|
|
|
|
|
for h in history: |
|
|
if isinstance(h, dict): messages.append(h) |
|
|
elif isinstance(h, list) and len(h) >= 2: |
|
|
messages.append({"role": "user", "content": str(h[0])}) |
|
|
messages.append({"role": "assistant", "content": str(h[1])}) |
|
|
messages.append({"role": "user", "content": message}) |
|
|
|
|
|
try: |
|
|
response = client.chat_completion(messages, max_tokens=1000, temperature=0.1).choices[0].message.content |
|
|
|
|
|
|
|
|
tool_tag = None |
|
|
if "[TOOL:" in response: |
|
|
tool_tag = response.split("[TOOL:")[1].split("]")[0] |
|
|
elif "get_player_report(" in response: |
|
|
arg = response.split("get_player_report(")[1].split(")")[0] |
|
|
tool_tag = f"get_player_report:{arg}" |
|
|
elif "find_hidden_gems(" in response: |
|
|
args = response.split("find_hidden_gems(")[1].split(")")[0] |
|
|
tool_tag = f"find_hidden_gems:{args}" |
|
|
|
|
|
if tool_tag: |
|
|
try: |
|
|
|
|
|
if "[TOOL:" in response: |
|
|
clean_resp = response.split("]")[0] + "]" |
|
|
else: |
|
|
clean_resp = response |
|
|
|
|
|
if ":" in tool_tag: |
|
|
func_name, args_raw = tool_tag.split(":", 1) |
|
|
args = [a.strip() for a in re.split(r'[,;|]', args_raw) if a.strip()] |
|
|
else: |
|
|
if "," in tool_tag: |
|
|
func_name = tool_tag.split("(")[0].strip() if "(" in tool_tag else tool_tag.split(",")[0] |
|
|
func_name, args = tool_tag.split(":")[0], [] |
|
|
else: |
|
|
func_name, args = tool_tag.strip(), [] |
|
|
|
|
|
tool_result = "Unknown Tool" |
|
|
if "get_player_report" in func_name: |
|
|
arg1 = args[0] if len(args) > 0 else "Haaland" |
|
|
tool_result = get_player_report(arg1) |
|
|
elif "compare_players" in func_name: |
|
|
arg1 = args[0] if len(args) > 0 else "Salah" |
|
|
arg2 = args[1] if len(args) > 1 else "Haaland" |
|
|
tool_result = compare_players(arg1, arg2) |
|
|
elif "find_hidden_gems" in func_name: |
|
|
pos = args[0] if len(args) > 0 else "MID" |
|
|
try: |
|
|
p_str = args[1] if len(args) > 1 else "15.0" |
|
|
clean_p = re.sub(r'[^\d.]', '', p_str) |
|
|
price = float(clean_p) |
|
|
except: price = 15.0 |
|
|
tool_result = find_hidden_gems(pos, price) |
|
|
|
|
|
messages.append({"role": "assistant", "content": clean_resp}) |
|
|
messages.append({"role": "user", "content": f"Tool Result:\n{tool_result}\n\nUsing this data, answer the user."}) |
|
|
|
|
|
stream = client.chat_completion(messages, max_tokens=1024, stream=True) |
|
|
full = "" |
|
|
for chunk in stream: |
|
|
if chunk.choices and len(chunk.choices) > 0: |
|
|
delta = chunk.choices[0].delta |
|
|
if delta.content: |
|
|
full += delta.content |
|
|
yield full |
|
|
except Exception as e: |
|
|
traceback.print_exc() |
|
|
yield f"⚠️ Tool Error: {e}" |
|
|
else: |
|
|
yield response |
|
|
except Exception as e: |
|
|
yield f"⚠️ API Error: {str(e)}" |
|
|
|
|
|
|
|
|
print("--- FPL AGENT STARTUP DIAGNOSTICS ---") |
|
|
check = fetch_bootstrap() |
|
|
if check: |
|
|
print(f"✅ Connection Established! Loaded {len(check['elements'])} players.") |
|
|
else: |
|
|
print("❌ Connection FAILED. Check internet or User-Agent.") |
|
|
print("---------------------------------------") |
|
|
|
|
|
|
|
|
with gr.Blocks(title="⚽️ FPL Scout Agent") as demo: |
|
|
gr.Markdown("# 🦁 FPL Scout Agent (Official)") |
|
|
gr.ChatInterface( |
|
|
fn=chat_logic, |
|
|
description="Your AI Assistant for Fantasy Premier League. Ask about Price, Form, Fixtures, and Differentials." |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
print("🚀 Launching...") |
|
|
demo.launch(theme=fpl_theme) |