Ralitza Mondal commited on
Commit
aaa201d
·
1 Parent(s): 641bcfa

Add multi-agent coach system (FAISS files to be uploaded separately)

Browse files
.gitattributes CHANGED
@@ -3,6 +3,7 @@
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
  *.bz2 filter=lfs diff=lfs merge=lfs -text
5
  *.ckpt filter=lfs diff=lfs merge=lfs -text
 
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
 
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
  *.bz2 filter=lfs diff=lfs merge=lfs -text
5
  *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.faiss filter=lfs diff=lfs merge=lfs -text
7
  *.ftz filter=lfs diff=lfs merge=lfs -text
8
  *.gz filter=lfs diff=lfs merge=lfs -text
9
  *.h5 filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ ENV/
26
+ env/
27
+
28
+ # Environment variables
29
+ .env
30
+
31
+ # IDE
32
+ .vscode/
33
+ .idea/
34
+ *.swp
35
+ *.swo
36
+ *~
37
+
38
+ # OS
39
+ .DS_Store
40
+ Thumbs.db
41
+
42
+ # Logs
43
+ logs/
44
+ *.log
45
+
46
+ # Testing
47
+ .pytest_cache/
48
+ .coverage
49
+ htmlcov/
50
+
51
+ # Temporary files
52
+ *.tmp
53
+ *.bak
54
+ knowledge_base/faiss_index/*.faiss
PUSH_INSTRUCTIONS.md ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ✅ YOUR FILES ARE READY TO PUSH!
2
+
3
+ All necessary files have been copied to:
4
+ `/Users/ralitzamondal/Documents/LolMultiAgent/`
5
+
6
+ ## 📦 Files Successfully Copied:
7
+
8
+ ✅ app.py (HF entry point)
9
+ ✅ multi_agent_coach.py (Main system - 60KB)
10
+ ✅ specialized_agents.py (Agents)
11
+ ✅ riot_api.py (Riot API)
12
+ ✅ youtube_scraper.py (YouTube)
13
+ ✅ requirements.txt (Dependencies)
14
+ ✅ README.md (HF documentation)
15
+ ✅ .gitignore (Git rules)
16
+ ✅ knowledge_base/faiss_index/index.faiss (202KB)
17
+ ✅ knowledge_base/faiss_index/index.pkl (14KB)
18
+
19
+ ## 🔐 Next Step: Push to Hugging Face
20
+
21
+ You need to authenticate with Hugging Face using a token:
22
+
23
+ ### Option 1: Use Hugging Face CLI (Recommended)
24
+
25
+ ```bash
26
+ cd /Users/ralitzamondal/Documents/LolMultiAgent
27
+ /Users/ralitzamondal/Documents/lol-coach-agent/venv/bin/huggingface-cli login
28
+ # Enter your token when prompted
29
+ git push
30
+ ```
31
+
32
+ ### Option 2: Use Git with Token
33
+
34
+ ```bash
35
+ cd /Users/ralitzamondal/Documents/LolMultiAgent
36
+ git remote set-url origin https://YOUR_USERNAME:YOUR_TOKEN@huggingface.co/spaces/Ralitza1/LolMultiAgent
37
+ git push
38
+ ```
39
+
40
+ ### Where to Get Your Token:
41
+
42
+ 1. Go to https://huggingface.co/settings/tokens
43
+ 2. Click "New token"
44
+ 3. Name: "LolMultiAgent"
45
+ 4. Type: "Write" (to push code)
46
+ 5. Copy the token
47
+
48
+ ## 🚀 After Pushing:
49
+
50
+ 1. Go to https://huggingface.co/spaces/Ralitza1/LolMultiAgent
51
+ 2. Wait 5-10 minutes for build
52
+ 3. Add API keys in Settings → Repository Secrets:
53
+ - OPENAI_API_KEY
54
+ - RIOT_API_KEY
55
+ - TAVILY_API_KEY
56
+ 4. Your Space will be live!
57
+
58
+ ## 📋 Quick Push Commands:
59
+
60
+ ```bash
61
+ cd /Users/ralitzamondal/Documents/LolMultiAgent
62
+
63
+ # Login to Hugging Face (do this once)
64
+ /Users/ralitzamondal/Documents/lol-coach-agent/venv/bin/huggingface-cli login
65
+
66
+ # Push to Hugging Face
67
+ git push
68
+
69
+ # If it asks for credentials again, use your HF token as password
70
+ ```
71
+
72
+ ## ⚠️ Current Status:
73
+
74
+ Your local changes are committed but NOT yet pushed to Hugging Face.
75
+ You need to authenticate and push to make them live.
76
+
77
+ ---
78
+
79
+ **All files are ready! Just need to push! 🎉**
README.md CHANGED
@@ -1,14 +1,59 @@
1
  ---
2
- title: LolMultiAgent
3
- emoji: 🚀
4
- colorFrom: purple
5
- colorTo: pink
6
  sdk: gradio
7
- sdk_version: 6.3.0
8
- app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: This app will help you with play better at league of legends
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: LoL Multi-Agent Coach
3
+ emoji: 🎮
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: "6.2.0"
8
+ app_file: multi_agent_coach.py
9
  pinned: false
10
  license: mit
11
+ short_description: AI-powered League of Legends coaching system
12
  ---
13
 
14
+ # League of Legends Multi-Agent Coach 🎮
15
+
16
+ A sophisticated multi-agent AI coaching system for League of Legends that provides:
17
+ - 🎯 Match analysis and performance insights
18
+ - 🛠️ Build recommendations with real-time meta data
19
+ - 🎬 Video guides and educational content
20
+ - 📚 LoL knowledge base with FAISS vector search
21
+ - 🎲 Pregame strategy (bans, picks, team comp analysis)
22
+
23
+ ## Features
24
+
25
+ - **5 Specialized AI Agents** working collaboratively
26
+ - **14 Tools** for comprehensive coaching
27
+ - **Real-time data** from Riot API, Tavily, and YouTube
28
+ - **FAISS Vector Database** for knowledge retrieval
29
+
30
+ ## Setup
31
+
32
+ This Space requires three API keys to be set as Secrets:
33
+
34
+ 1. **OPENAI_API_KEY** - Get from https://platform.openai.com/
35
+ 2. **RIOT_API_KEY** - Get from https://developer.riotgames.com/
36
+ 3. **TAVILY_API_KEY** - Get from https://tavily.com/
37
+
38
+ Optional: Configure your summoner information:
39
+ - **SUMMONER_NAME** - Your League summoner name
40
+ - **SUMMONER_TAG** - Your tagline (e.g., NA1)
41
+ - **REGION** - Your region (e.g., na1)
42
+
43
+ ## Usage
44
+
45
+ Simply type your questions in the chat interface:
46
+ - "Analyze my last 5 games"
47
+ - "What's the best build for Yasuo?"
48
+ - "Find Yasuo vs Zed matchup videos"
49
+ - "What should I ban if playing mid?"
50
+ - "Analyze this team comp: Darius, Lee Sin, Ahri, Jinx, Thresh"
51
+
52
+ ## Technology Stack
53
+
54
+ - **LangChain/LangGraph** - Multi-agent framework
55
+ - **OpenAI GPT-4** - Language model
56
+ - **Gradio 6.2.0** - Web interface
57
+ - **FAISS** - Vector database
58
+ - **Riot API** - Live game data
59
+ - **Tavily** - Real-time web search
app.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ League of Legends Multi-Agent Coach - Hugging Face Spaces Version
3
+ This is a wrapper for deploying to Hugging Face Spaces.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+
9
+ # Set environment variables from Hugging Face Secrets
10
+ # These will be available in the Hugging Face Spaces settings
11
+ if not os.getenv("OPENAI_API_KEY"):
12
+ print("⚠️ WARNING: OPENAI_API_KEY not found in environment!")
13
+ if not os.getenv("RIOT_API_KEY"):
14
+ print("⚠️ WARNING: RIOT_API_KEY not found in environment!")
15
+ if not os.getenv("TAVILY_API_KEY"):
16
+ print("⚠️ WARNING: TAVILY_API_KEY not found in environment!")
17
+
18
+ # Import the main application
19
+ from multi_agent_coach import create_multi_agent_coach, create_gradio_interface
20
+
21
+ # Create the coach system
22
+ print("🚀 Initializing Multi-Agent LoL Coach System for Hugging Face...")
23
+ coach = create_multi_agent_coach()
24
+
25
+ # Create the Gradio interface
26
+ print("🎨 Creating Gradio interface...")
27
+ demo = create_gradio_interface(coach)
28
+
29
+ # Launch with Hugging Face-compatible settings
30
+ if __name__ == "__main__":
31
+ demo.launch(
32
+ server_name="0.0.0.0", # Required for Hugging Face Spaces
33
+ server_port=7860,
34
+ share=False # Not needed on HF Spaces
35
+ )
knowledge_base/faiss_index/index.pkl ADDED
Binary file (14.3 kB). View file
 
multi_agent_coach.py ADDED
@@ -0,0 +1,1315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Multi-Agent LoL Coach System
3
+ Main integration file connecting router, agents, and orchestrator.
4
+ """
5
+
6
+ import os
7
+ import logging
8
+ from datetime import datetime
9
+ from dotenv import load_dotenv
10
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
11
+ from langchain.tools import tool
12
+ from langchain_community.vectorstores import FAISS
13
+ from tavily import TavilyClient
14
+
15
+ # Import API clients and data fetchers
16
+ from riot_api import RiotAPI
17
+ from data_fetchers import get_optimal_build_ugg, get_champion_stats
18
+ from youtube_scraper import YouTubeScraper
19
+
20
+ # Import multi-agent components
21
+ from multi_agent_router import create_router, QueryRouter
22
+ from specialized_agents import create_specialized_agents, BaseLoLAgent
23
+ from multi_agent_orchestrator import create_orchestrator, MultiAgentOrchestrator
24
+
25
+ # Create logs directory if it doesn't exist
26
+ log_dir = os.path.join(os.path.dirname(__file__), 'logs')
27
+ os.makedirs(log_dir, exist_ok=True)
28
+
29
+ # Setup logging
30
+ logging.basicConfig(
31
+ level=logging.INFO,
32
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
33
+ handlers=[
34
+ logging.FileHandler(os.path.join(log_dir, f'multi_agent_{datetime.now().strftime("%Y%m%d")}.log')),
35
+ logging.StreamHandler()
36
+ ]
37
+ )
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ def load_knowledge_base_retriever(openai_api_key: str):
42
+ """
43
+ Load the LoL knowledge base FAISS vector store as a retriever.
44
+
45
+ Args:
46
+ openai_api_key: OpenAI API key for embeddings
47
+
48
+ Returns:
49
+ FAISS retriever or None if knowledge base doesn't exist
50
+ """
51
+ embeddings = OpenAIEmbeddings(api_key=openai_api_key)
52
+
53
+ knowledge_base_path = "./knowledge_base/faiss_index"
54
+ if os.path.exists(knowledge_base_path):
55
+ logger.info(f"Loading FAISS knowledge base from {knowledge_base_path}")
56
+ try:
57
+ vector_store = FAISS.load_local(
58
+ knowledge_base_path,
59
+ embeddings,
60
+ allow_dangerous_deserialization=True
61
+ )
62
+ retriever = vector_store.as_retriever(search_kwargs={"k": 5})
63
+ logger.info("✅ FAISS knowledge base loaded successfully")
64
+ return retriever
65
+ except Exception as e:
66
+ logger.error(f"❌ Error loading FAISS knowledge base: {e}")
67
+ return None
68
+ else:
69
+ logger.warning(f"⚠️ Knowledge base not found at {knowledge_base_path}")
70
+ logger.warning(" Run 'python create_lol_knowledge_base.py' to create it")
71
+ return None
72
+
73
+
74
+ # Import existing tools (from original lol_coach_agent.py)
75
+ # These will be distributed among specialized agents
76
+
77
+
78
+ class MultiAgentLoLCoach:
79
+ """
80
+ Multi-agent League of Legends coaching system with intelligent routing.
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ openai_api_key: str,
86
+ riot_api_key: str,
87
+ region: str = "na1",
88
+ routing_value: str = "americas",
89
+ summoner_name: str = None,
90
+ summoner_tag: str = None,
91
+ additional_summoners: list = None
92
+ ):
93
+ # Initialize LLM
94
+ self.llm = ChatOpenAI(
95
+ api_key=openai_api_key,
96
+ model="gpt-4o-mini",
97
+ temperature=0.3
98
+ )
99
+
100
+ # Store API keys and user info for tools
101
+ self.riot_api_key = riot_api_key
102
+ self.region = region
103
+ self.routing_value = routing_value
104
+ self.summoner_name = summoner_name
105
+ self.summoner_tag = summoner_tag
106
+ self.additional_summoners = additional_summoners or []
107
+
108
+ # Initialize API clients
109
+ self.riot_api = RiotAPI(api_key=riot_api_key, region=region, routing=routing_value)
110
+ self.youtube_scraper = YouTubeScraper()
111
+
112
+ # Initialize Tavily client for web search
113
+ tavily_api_key = os.getenv("TAVILY_API_KEY")
114
+ self.tavily_client = TavilyClient(api_key=tavily_api_key) if tavily_api_key else None
115
+
116
+ # Initialize components
117
+ logger.info("Initializing Multi-Agent LoL Coach System...")
118
+ print("🚀 Initializing Multi-Agent LoL Coach System...")
119
+
120
+ # Load FAISS knowledge base
121
+ self.knowledge_retriever = load_knowledge_base_retriever(openai_api_key)
122
+ if self.knowledge_retriever:
123
+ print(" ✅ FAISS knowledge base loaded")
124
+ else:
125
+ print(" ⚠️ FAISS knowledge base not available")
126
+
127
+ # Display tracked summoners
128
+ if summoner_name and summoner_tag:
129
+ primary_summoner = f"{summoner_name}#{summoner_tag}"
130
+ logger.info(f"Primary Summoner: {primary_summoner} ({region.upper()})")
131
+ print(f" 👤 Primary Summoner: {primary_summoner} ({region.upper()})")
132
+
133
+ if additional_summoners:
134
+ logger.info(f"Additional Summoners: {', '.join(additional_summoners)}")
135
+ print(f" 👥 Additional Summoners: {', '.join(additional_summoners)}")
136
+
137
+ # 1. Create router
138
+ self.router = create_router(openai_api_key)
139
+ logger.info("Router initialized successfully")
140
+ print(" ✅ Router initialized")
141
+
142
+ # 2. Organize tools by category
143
+ tools = self._organize_tools()
144
+
145
+ # 3. Create specialized agents
146
+ self.agents = create_specialized_agents(self.llm, tools)
147
+ logger.info(f"{len(self.agents)} specialized agents created")
148
+ print(f" ✅ {len(self.agents)} specialized agents created:")
149
+ for name, agent in self.agents.items():
150
+ info = agent.get_info()
151
+ print(f" • {name}: {info['tool_count']} tools")
152
+
153
+ # 4. Create orchestrator
154
+ self.orchestrator = create_orchestrator(self.llm, self.agents)
155
+ print(" ✅ Orchestrator initialized")
156
+
157
+ print("\n✨ Multi-Agent System Ready!\n")
158
+
159
+ def _organize_tools(self) -> dict:
160
+ """
161
+ Organize all tools into categories for distribution to specialized agents.
162
+
163
+ Note: This imports tools from the original lol_coach_agent.py
164
+ We'll need to refactor those tools to be importable.
165
+ """
166
+ # Helper function to get tagline for summoner
167
+ def get_tagline_for_summoner(summoner_name: str) -> str:
168
+ """Look up the correct tagline for a summoner name."""
169
+ summoner_name_lower = summoner_name.lower()
170
+ tracked_summoners = [(self.summoner_name, self.summoner_tag)]
171
+ for summoner in self.additional_summoners:
172
+ if "#" in summoner:
173
+ name, tag = summoner.split("#", 1)
174
+ tracked_summoners.append((name.strip(), tag.strip()))
175
+
176
+ for name, tag in tracked_summoners:
177
+ if name.lower() == summoner_name_lower:
178
+ return tag
179
+ return self.summoner_tag or "NA1"
180
+
181
+ # === MATCH ANALYSIS TOOLS ===
182
+ match_tools = []
183
+
184
+ @tool
185
+ def get_summoner_profile(summoner_name: str = None, tag_line: str = None) -> str:
186
+ """
187
+ Get summoner profile information including level, rank, and basic stats.
188
+ If no summoner_name provided, uses the configured default summoner.
189
+ """
190
+ if summoner_name is None:
191
+ summoner_name = self.summoner_name
192
+ if tag_line is None:
193
+ tag_line = get_tagline_for_summoner(summoner_name)
194
+
195
+ try:
196
+ account_info = self.riot_api.get_account_by_riot_id(summoner_name, tag_line)
197
+ if not account_info:
198
+ return f"❌ Account '{summoner_name}#{tag_line}' not found."
199
+
200
+ puuid = account_info.get('puuid')
201
+ summoner_info = self.riot_api.get_summoner_by_puuid(puuid)
202
+
203
+ if not summoner_info or 'id' not in summoner_info:
204
+ return f"❌ Summoner data not found for '{summoner_name}#{tag_line}'."
205
+
206
+ ranked_info = self.riot_api.get_ranked_stats(summoner_info.get('id'))
207
+
208
+ result = f"📊 **Summoner Profile: {summoner_name}#{tag_line}**\n\n"
209
+ result += f"• Level: {summoner_info.get('summonerLevel', 'N/A')}\n\n"
210
+
211
+ if ranked_info:
212
+ for queue in ranked_info:
213
+ queue_type = queue.get('queueType', 'Unknown')
214
+ tier = queue.get('tier', 'Unranked')
215
+ rank = queue.get('rank', '')
216
+ lp = queue.get('leaguePoints', 0)
217
+ wins = queue.get('wins', 0)
218
+ losses = queue.get('losses', 0)
219
+ win_rate = (wins / (wins + losses) * 100) if (wins + losses) > 0 else 0
220
+
221
+ result += f"**{queue_type}**\n"
222
+ result += f"• Rank: {tier} {rank} ({lp} LP)\n"
223
+ result += f"• Win Rate: {wins}W / {losses}L ({win_rate:.1f}%)\n\n"
224
+ else:
225
+ result += "• No ranked data available\n"
226
+
227
+ return result
228
+ except Exception as e:
229
+ logger.error(f"Error fetching summoner profile: {e}")
230
+ return f"❌ Error fetching summoner profile: {str(e)}"
231
+
232
+ match_tools.append(get_summoner_profile)
233
+
234
+ @tool
235
+ def analyze_recent_matches(summoner_name: str = None, tag_line: str = None, num_matches: int = 10) -> str:
236
+ """
237
+ Analyze recent match history and provide performance insights.
238
+ Shows KDA, win rate, CS, damage, and identifies patterns.
239
+ """
240
+ if summoner_name is None:
241
+ summoner_name = self.summoner_name
242
+ if tag_line is None:
243
+ tag_line = get_tagline_for_summoner(summoner_name)
244
+
245
+ try:
246
+ account_info = self.riot_api.get_account_by_riot_id(summoner_name, tag_line)
247
+ if not account_info:
248
+ return f"❌ Account '{summoner_name}#{tag_line}' not found."
249
+
250
+ puuid = account_info.get('puuid')
251
+ match_ids = self.riot_api.get_match_history(puuid, count=num_matches)
252
+
253
+ if not match_ids:
254
+ return "❌ No match history found."
255
+
256
+ matches_data = []
257
+ for match_id in match_ids[:num_matches]:
258
+ match_detail = self.riot_api.get_match_details(match_id, puuid)
259
+ if match_detail:
260
+ matches_data.append(match_detail)
261
+
262
+ if not matches_data:
263
+ return "❌ Could not retrieve match details."
264
+
265
+ # Calculate statistics
266
+ total_games = len(matches_data)
267
+ wins = sum(1 for m in matches_data if m['win'])
268
+ win_rate = (wins / total_games * 100) if total_games > 0 else 0
269
+
270
+ avg_kills = sum(m['kills'] for m in matches_data) / total_games
271
+ avg_deaths = sum(m['deaths'] for m in matches_data) / total_games
272
+ avg_assists = sum(m['assists'] for m in matches_data) / total_games
273
+ avg_kda = ((avg_kills + avg_assists) / avg_deaths) if avg_deaths > 0 else 0
274
+
275
+ avg_cs = sum(m['cs'] for m in matches_data) / total_games
276
+ avg_damage = sum(m['damage'] for m in matches_data) / total_games
277
+
278
+ # Champion frequency
279
+ champion_counts = {}
280
+ for match in matches_data:
281
+ champ = match['champion']
282
+ if champ not in champion_counts:
283
+ champion_counts[champ] = {'games': 0, 'wins': 0}
284
+ champion_counts[champ]['games'] += 1
285
+ if match['win']:
286
+ champion_counts[champ]['wins'] += 1
287
+
288
+ result = f"🎮 **Match Analysis: Last {total_games} Games**\n\n"
289
+ result += f"**Overall Performance:**\n"
290
+ result += f"• Win Rate: {wins}W / {total_games - wins}L ({win_rate:.1f}%)\n"
291
+ result += f"• Average KDA: {avg_kills:.1f} / {avg_deaths:.1f} / {avg_assists:.1f} ({avg_kda:.2f} ratio)\n"
292
+ result += f"• Average CS: {avg_cs:.0f}\n"
293
+ result += f"• Average Damage: {avg_damage:,.0f}\n\n"
294
+
295
+ result += "**Champion Pool:**\n"
296
+ for champ, stats in sorted(champion_counts.items(), key=lambda x: x[1]['games'], reverse=True):
297
+ champ_wr = (stats['wins'] / stats['games'] * 100) if stats['games'] > 0 else 0
298
+ result += f"• {champ}: {stats['games']} games, {stats['wins']}W ({champ_wr:.0f}% WR)\n"
299
+
300
+ result += "\n**Recent Matches:**\n"
301
+ for i, match in enumerate(matches_data[:5], 1):
302
+ result_icon = "✅" if match['win'] else "❌"
303
+ kda = f"{match['kills']}/{match['deaths']}/{match['assists']}"
304
+ result += f"{i}. {result_icon} {match['champion']} - {kda} - {match['cs']} CS\n"
305
+
306
+ return result
307
+ except Exception as e:
308
+ logger.error(f"Error analyzing matches: {e}")
309
+ return f"❌ Error analyzing matches: {str(e)}"
310
+
311
+ match_tools.append(analyze_recent_matches)
312
+
313
+ # === KNOWLEDGE BASE TOOLS ===
314
+ knowledge_tools = []
315
+
316
+ # Add FAISS knowledge base search
317
+ if self.knowledge_retriever:
318
+ @tool
319
+ def search_lol_knowledge(query: str) -> str:
320
+ """
321
+ Search the League of Legends knowledge base for information about champions,
322
+ items, runes, game mechanics, strategies, and meta information.
323
+ """
324
+ try:
325
+ docs = self.knowledge_retriever.invoke(query)
326
+ if docs:
327
+ results = []
328
+ for i, doc in enumerate(docs, 1):
329
+ results.append(f"[Source {i}]\n{doc.page_content}\n")
330
+ return "\n".join(results)
331
+ else:
332
+ return "No relevant information found in the knowledge base."
333
+ except Exception as e:
334
+ logger.error(f"Error searching knowledge base: {e}")
335
+ return f"Error searching knowledge base: {str(e)}"
336
+
337
+ knowledge_tools.append(search_lol_knowledge)
338
+
339
+ # Add Tavily web search for real-time LoL information
340
+ if self.tavily_client:
341
+ @tool
342
+ def search_web_lol_info(query: str) -> str:
343
+ """
344
+ Search the web for real-time League of Legends information including:
345
+ - Current meta analysis and tier lists
346
+ - Latest patch notes and balance changes
347
+ - Pro player builds and strategies
348
+ - Champion guides from popular sites
349
+ - Recent tournament results
350
+
351
+ Use this for up-to-date information that may not be in the knowledge base.
352
+ """
353
+ try:
354
+ # Add "League of Legends" to the query for better results
355
+ search_query = f"League of Legends {query}"
356
+ logger.info(f"Tavily search: {search_query}")
357
+
358
+ response = self.tavily_client.search(
359
+ query=search_query,
360
+ search_depth="advanced",
361
+ max_results=5
362
+ )
363
+
364
+ if not response or 'results' not in response:
365
+ return "❌ No web results found."
366
+
367
+ results = []
368
+ for i, result in enumerate(response['results'][:5], 1):
369
+ title = result.get('title', 'No title')
370
+ content = result.get('content', 'No content')
371
+ url = result.get('url', 'No URL')
372
+
373
+ results.append(f"**[{i}] {title}**\n{content}\n🔗 Source: {url}\n")
374
+
375
+ if results:
376
+ return "🌐 **Web Search Results:**\n\n" + "\n".join(results)
377
+ else:
378
+ return "❌ No relevant web results found."
379
+
380
+ except Exception as e:
381
+ logger.error(f"Error searching web with Tavily: {e}")
382
+ return f"❌ Error searching web: {str(e)}"
383
+
384
+ knowledge_tools.append(search_web_lol_info)
385
+
386
+ # === VIDEO GUIDE TOOLS ===
387
+ video_tools = []
388
+
389
+ @tool
390
+ def find_champion_guides(champion_name: str, max_results: int = 5) -> str:
391
+ """
392
+ Find YouTube video guides for a specific champion.
393
+ Returns video titles, channels, durations, and links to helpful guides.
394
+ """
395
+ try:
396
+ query = f"League of Legends {champion_name} guide season 14 2024"
397
+ videos = self.youtube_scraper.search_videos(query, max_results)
398
+
399
+ if not videos:
400
+ return f"❌ No guide videos found for {champion_name}."
401
+
402
+ result = f"🎬 **YouTube Guides for {champion_name}**\n\n"
403
+ result += f"Found {len(videos)} helpful guides:\n\n"
404
+ result += self.youtube_scraper.format_video_list(videos)
405
+ result += "\n💡 **Tip:** Watch these guides to learn optimal combos, positioning, and decision-making!"
406
+
407
+ return result
408
+ except Exception as e:
409
+ logger.error(f"Error finding champion guides: {e}")
410
+ return f"❌ Error finding champion guides: {str(e)}"
411
+
412
+ video_tools.append(find_champion_guides)
413
+
414
+ @tool
415
+ def find_matchup_videos(champion_name: str, enemy_champion: str, max_results: int = 3) -> str:
416
+ """
417
+ Find YouTube videos showing how to play a specific matchup.
418
+ Shows real gameplay examples of your champion vs enemy champion.
419
+ """
420
+ try:
421
+ query = f"League of Legends {champion_name} vs {enemy_champion} matchup guide"
422
+ videos = self.youtube_scraper.search_videos(query, max_results)
423
+
424
+ if not videos:
425
+ return f"❌ No matchup videos found for {champion_name} vs {enemy_champion}."
426
+
427
+ result = f"⚔️ **{champion_name} vs {enemy_champion} - Matchup Videos**\n\n"
428
+ result += f"Watch these to learn how to play this matchup:\n\n"
429
+ result += self.youtube_scraper.format_video_list(videos)
430
+ result += f"\n💡 **Watch for:** Trading patterns, wave management, powerspikes, and how to exploit {enemy_champion}'s weaknesses"
431
+
432
+ return result
433
+ except Exception as e:
434
+ logger.error(f"Error finding matchup videos: {e}")
435
+ return f"❌ Error finding matchup videos: {str(e)}"
436
+
437
+ video_tools.append(find_matchup_videos)
438
+
439
+ @tool
440
+ def find_educational_videos(topic: str, max_results: int = 5) -> str:
441
+ """
442
+ Find educational League of Legends videos on a specific topic.
443
+ Topics can include: wave management, trading, macro, team fighting, vision control, etc.
444
+ """
445
+ try:
446
+ query = f"League of Legends {topic} guide tutorial"
447
+ videos = self.youtube_scraper.search_videos(query, max_results)
448
+
449
+ if not videos:
450
+ return f"❌ No educational videos found for '{topic}'."
451
+
452
+ result = f"📚 **Learning Resources: {topic}**\n\n"
453
+ result += self.youtube_scraper.format_video_list(videos)
454
+ result += f"\n🎓 **Study tip:** Take notes while watching and practice these concepts in your next games!"
455
+
456
+ return result
457
+ except Exception as e:
458
+ logger.error(f"Error finding educational videos: {e}")
459
+ return f"❌ Error finding educational videos: {str(e)}"
460
+
461
+ video_tools.append(find_educational_videos)
462
+
463
+ # === BUILD ADVISOR TOOLS ===
464
+ build_tools = []
465
+
466
+ @tool
467
+ def get_optimal_build(champion_name: str, role: str = "any", enemy_matchup: str = None) -> str:
468
+ """
469
+ Get optimal item build, runes, and skill order for a champion based on current meta.
470
+ Uses real-time data from Tavily to find the best builds being used by high-elo players.
471
+ """
472
+ try:
473
+ if not self.tavily_client:
474
+ return "❌ Tavily API not configured. Cannot fetch build recommendations."
475
+
476
+ # Build search query
477
+ query = f"{champion_name} {role} build items runes skill order season 14 2024 high elo pro"
478
+ if enemy_matchup:
479
+ query += f" vs {enemy_matchup}"
480
+
481
+ logger.info(f"Fetching optimal build: {query}")
482
+
483
+ response = self.tavily_client.search(
484
+ query=query,
485
+ search_depth="advanced",
486
+ max_results=5
487
+ )
488
+
489
+ if not response or 'results' not in response:
490
+ return f"❌ Could not find build information for {champion_name}."
491
+
492
+ result = f"🛠️ **Optimal Build for {champion_name}**"
493
+ if role and role != "any":
494
+ result += f" ({role.capitalize()})"
495
+ if enemy_matchup:
496
+ result += f" vs {enemy_matchup}"
497
+ result += "\n\n"
498
+
499
+ result += "**Current Meta Build Information:**\n\n"
500
+ for i, item in enumerate(response['results'][:3], 1):
501
+ result += f"{i}. **{item['title']}**\n"
502
+ result += f" {item['content'][:250]}...\n"
503
+ result += f" 🔗 Source: {item['url']}\n\n"
504
+
505
+ result += "💡 **Tip:** Look for common patterns across sources - consistent recommendations indicate proven strategies!"
506
+
507
+ return result
508
+ except Exception as e:
509
+ logger.error(f"Error fetching optimal build: {e}")
510
+ return f"❌ Error fetching build: {str(e)}"
511
+
512
+ build_tools.append(get_optimal_build)
513
+
514
+ @tool
515
+ def get_champion_matchups(champion_name: str) -> str:
516
+ """
517
+ Get matchup information including counters, favorable matchups, and tips.
518
+ Uses real-time data to find current meta matchup analysis.
519
+ """
520
+ try:
521
+ if not self.tavily_client:
522
+ return "❌ Tavily API not configured. Cannot fetch matchup data."
523
+
524
+ query = f"{champion_name} counters matchups tier list strong against weak against"
525
+ logger.info(f"Fetching matchup data: {query}")
526
+
527
+ response = self.tavily_client.search(
528
+ query=query,
529
+ search_depth="advanced",
530
+ max_results=5
531
+ )
532
+
533
+ if not response or 'results' not in response:
534
+ return f"❌ Could not find matchup information for {champion_name}."
535
+
536
+ result = f"⚔️ **{champion_name} Matchup Analysis**\n\n"
537
+ result += "**Current Meta Analysis:**\n\n"
538
+
539
+ for i, item in enumerate(response['results'][:3], 1):
540
+ result += f"{i}. **{item['title']}**\n"
541
+ result += f" {item['content'][:250]}...\n"
542
+ result += f" 🔗 {item['url']}\n\n"
543
+
544
+ result += "💡 **Tip:** Study your hardest matchups and learn how pros play them!"
545
+
546
+ return result
547
+ except Exception as e:
548
+ logger.error(f"Error fetching matchups: {e}")
549
+ return f"❌ Error fetching matchups: {str(e)}"
550
+
551
+ build_tools.append(get_champion_matchups)
552
+
553
+ @tool
554
+ def get_personalized_build_advice(champion_name: str, summoner_name: str = None) -> str:
555
+ """
556
+ Get personalized build recommendations based on the summoner's match history and playstyle.
557
+ Analyzes recent performance to suggest build adaptations.
558
+ """
559
+ try:
560
+ # Get summoner's recent matches to analyze playstyle
561
+ summoner_context = ""
562
+ if summoner_name or self.summoner_name:
563
+ name = summoner_name or self.summoner_name
564
+ tagline = self.get_tagline_for_summoner(name)
565
+
566
+ logger.info(f"Analyzing {name}'s playstyle for personalized build")
567
+
568
+ summoner_info = self.riot_api.get_summoner(name, tagline)
569
+ if summoner_info and 'puuid' in summoner_info:
570
+ matches = self.riot_api.get_match_history(summoner_info['puuid'], count=5)
571
+
572
+ if matches:
573
+ # Analyze playstyle from recent matches
574
+ total_kills = 0
575
+ total_deaths = 0
576
+ total_damage = 0
577
+ game_count = 0
578
+
579
+ for match_id in matches[:5]:
580
+ match_detail = self.riot_api.get_match_details(match_id, summoner_info['puuid'])
581
+ if match_detail:
582
+ total_kills += match_detail.get('kills', 0)
583
+ total_deaths += match_detail.get('deaths', 0)
584
+ total_damage += match_detail.get('damage', 0)
585
+ game_count += 1
586
+
587
+ if game_count > 0:
588
+ avg_kda = ((total_kills) / total_deaths) if total_deaths > 0 else total_kills
589
+ avg_damage = total_damage / game_count
590
+
591
+ if avg_kda > 4:
592
+ summoner_context = f"\n**Your Playstyle:** Aggressive carry-style (KDA: {avg_kda:.1f}). Consider damage-focused builds."
593
+ elif avg_kda < 2:
594
+ summoner_context = f"\n**Your Playstyle:** Struggling with deaths (KDA: {avg_kda:.1f}). Consider defensive/survivability items."
595
+ else:
596
+ summoner_context = f"\n**Your Playstyle:** Balanced playstyle (KDA: {avg_kda:.1f}). Standard meta builds work well."
597
+
598
+ if avg_damage > 20000:
599
+ summoner_context += f"\n**Damage Profile:** High damage dealer ({avg_damage:,.0f} avg). Keep prioritizing damage."
600
+ elif avg_damage < 12000:
601
+ summoner_context += f"\n**Damage Profile:** Lower damage output ({avg_damage:,.0f} avg). Focus on damage items and positioning."
602
+
603
+ # Get meta build with personalized context
604
+ if not self.tavily_client:
605
+ return "❌ Tavily API not configured."
606
+
607
+ query = f"{champion_name} build recommendations playstyle adaptations when ahead when behind"
608
+ logger.info(f"Fetching personalized build advice: {query}")
609
+
610
+ response = self.tavily_client.search(
611
+ query=query,
612
+ search_depth="advanced",
613
+ max_results=4
614
+ )
615
+
616
+ result = f"🎯 **Personalized Build Advice for {champion_name}**\n"
617
+ result += summoner_context
618
+ result += "\n\n**Build Adaptations:**\n\n"
619
+
620
+ if response and 'results' in response:
621
+ for i, item in enumerate(response['results'][:3], 1):
622
+ result += f"{i}. **{item['title']}**\n"
623
+ result += f" {item['content'][:200]}...\n"
624
+ result += f" 🔗 {item['url']}\n\n"
625
+
626
+ result += "\n💡 **Remember:** Adapt your build based on game state - build defensively when behind, offensively when ahead!"
627
+
628
+ return result
629
+ except Exception as e:
630
+ logger.error(f"Error getting personalized build advice: {e}")
631
+ return f"❌ Error: {str(e)}"
632
+
633
+ build_tools.append(get_personalized_build_advice)
634
+
635
+ # === PREGAME STRATEGY TOOLS ===
636
+ pregame_tools = []
637
+
638
+ @tool
639
+ def recommend_bans(enemy_picks: str = None, your_role: str = None) -> str:
640
+ """
641
+ Recommend champions to ban based on current meta and enemy team picks.
642
+ Helps you ban OP champions or counter enemy team composition.
643
+
644
+ Args:
645
+ enemy_picks: Comma-separated list of champions enemy has already picked (optional)
646
+ your_role: Your intended role to ban lane counters (optional)
647
+ """
648
+ try:
649
+ if not self.tavily_client:
650
+ return "❌ Tavily API not configured. Cannot fetch ban recommendations."
651
+
652
+ # Build query based on context
653
+ query = "League of Legends season 14 2024 best bans meta OP champions tier list"
654
+ if enemy_picks:
655
+ query += f" synergy with {enemy_picks}"
656
+ if your_role:
657
+ query += f" {your_role} lane counters"
658
+
659
+ logger.info(f"Fetching ban recommendations: {query}")
660
+
661
+ response = self.tavily_client.search(
662
+ query=query,
663
+ search_depth="advanced",
664
+ max_results=5
665
+ )
666
+
667
+ if not response or 'results' not in response:
668
+ return "❌ Could not fetch ban recommendations."
669
+
670
+ result = "🚫 **Ban Recommendations**\n\n"
671
+
672
+ if enemy_picks:
673
+ result += f"**Enemy Picks:** {enemy_picks}\n"
674
+ if your_role:
675
+ result += f"**Your Role:** {your_role}\n"
676
+
677
+ result += "\n**Meta Analysis & Ban Priority:**\n\n"
678
+
679
+ for i, item in enumerate(response['results'][:4], 1):
680
+ result += f"{i}. **{item['title']}**\n"
681
+ result += f" {item['content'][:220]}...\n"
682
+ result += f" 🔗 {item['url']}\n\n"
683
+
684
+ result += "💡 **Ban Strategy Tips:**\n"
685
+ result += "• Ban champions that counter your main champion\n"
686
+ result += "• Ban meta OP champions with high win rates\n"
687
+ result += "• Ban champions that synergize well with enemy picks\n"
688
+ result += "• Consider banning champions your team struggles against"
689
+
690
+ return result
691
+ except Exception as e:
692
+ logger.error(f"Error recommending bans: {e}")
693
+ return f"❌ Error: {str(e)}"
694
+
695
+ pregame_tools.append(recommend_bans)
696
+
697
+ @tool
698
+ def suggest_champion_pick(
699
+ role: str,
700
+ team_picks: str = None,
701
+ enemy_picks: str = None,
702
+ preferred_playstyle: str = None
703
+ ) -> str:
704
+ """
705
+ Suggest the best champion to pick for your role based on team composition and enemy picks.
706
+
707
+ Args:
708
+ role: Your role (top, jungle, mid, adc, support)
709
+ team_picks: Comma-separated list of what your team has picked (optional)
710
+ enemy_picks: Comma-separated list of enemy team picks (optional)
711
+ preferred_playstyle: Your preferred playstyle (aggressive, defensive, utility, etc.)
712
+ """
713
+ try:
714
+ if not self.tavily_client:
715
+ return "❌ Tavily API not configured."
716
+
717
+ # Build comprehensive query
718
+ query = f"League of Legends best {role} champions season 14 2024"
719
+
720
+ context_parts = []
721
+ if team_picks:
722
+ context_parts.append(f"team composition {team_picks}")
723
+ if enemy_picks:
724
+ context_parts.append(f"counter picks against {enemy_picks}")
725
+ if preferred_playstyle:
726
+ context_parts.append(f"{preferred_playstyle} playstyle")
727
+
728
+ if context_parts:
729
+ query += " " + " ".join(context_parts)
730
+
731
+ query += " meta tier list synergy"
732
+
733
+ logger.info(f"Suggesting champion pick: {query}")
734
+
735
+ response = self.tavily_client.search(
736
+ query=query,
737
+ search_depth="advanced",
738
+ max_results=5
739
+ )
740
+
741
+ if not response or 'results' not in response:
742
+ return f"❌ Could not find champion recommendations for {role}."
743
+
744
+ result = f"🎯 **Champion Pick Recommendation for {role.upper()}**\n\n"
745
+
746
+ if team_picks:
747
+ result += f"**Your Team:** {team_picks}\n"
748
+ if enemy_picks:
749
+ result += f"**Enemy Team:** {enemy_picks}\n"
750
+ if preferred_playstyle:
751
+ result += f"**Playstyle:** {preferred_playstyle}\n"
752
+
753
+ result += "\n**Top Recommendations:**\n\n"
754
+
755
+ for i, item in enumerate(response['results'][:4], 1):
756
+ result += f"{i}. **{item['title']}**\n"
757
+ result += f" {item['content'][:220]}...\n"
758
+ result += f" 🔗 {item['url']}\n\n"
759
+
760
+ result += "💡 **Pick Strategy:**\n"
761
+ result += "✓ Pick champions that synergize with your team\n"
762
+ result += "✓ Pick counter-picks when possible\n"
763
+ result += "✓ Pick champions you're comfortable playing\n"
764
+ result += "✓ Consider team needs (AP/AD damage, tankiness, engage/disengage)"
765
+
766
+ return result
767
+ except Exception as e:
768
+ logger.error(f"Error suggesting champion pick: {e}")
769
+ return f"❌ Error: {str(e)}"
770
+
771
+ pregame_tools.append(suggest_champion_pick)
772
+
773
+ @tool
774
+ def analyze_team_composition(
775
+ your_team: str,
776
+ enemy_team: str = None
777
+ ) -> str:
778
+ """
779
+ Analyze team composition to identify win conditions, strengths, weaknesses, and strategy.
780
+
781
+ Args:
782
+ your_team: Comma-separated list of your team's champions (e.g., "Darius, Lee Sin, Ahri, Jinx, Thresh")
783
+ enemy_team: Comma-separated list of enemy champions (optional)
784
+ """
785
+ try:
786
+ if not self.tavily_client:
787
+ return "❌ Tavily API not configured."
788
+
789
+ # Analyze team comp
790
+ query = f"League of Legends team composition analysis {your_team}"
791
+ if enemy_team:
792
+ query += f" vs {enemy_team} matchup"
793
+ query += " win condition strategy strengths weaknesses"
794
+
795
+ logger.info(f"Analyzing team composition: {query}")
796
+
797
+ response = self.tavily_client.search(
798
+ query=query,
799
+ search_depth="advanced",
800
+ max_results=5
801
+ )
802
+
803
+ result = f"📊 **Team Composition Analysis**\n\n"
804
+ result += f"**Your Team:** {your_team}\n"
805
+ if enemy_team:
806
+ result += f"**Enemy Team:** {enemy_team}\n"
807
+
808
+ result += "\n**Composition Analysis:**\n\n"
809
+
810
+ if response and 'results' in response:
811
+ for i, item in enumerate(response['results'][:3], 1):
812
+ result += f"{i}. **{item['title']}**\n"
813
+ result += f" {item['content'][:220]}...\n"
814
+ result += f" 🔗 {item['url']}\n\n"
815
+
816
+ # Add basic analysis framework
817
+ result += "\n**Key Strategic Considerations:**\n\n"
818
+ result += "🎯 **Win Conditions:**\n"
819
+ result += "• Identify your team's power spikes (early/mid/late game)\n"
820
+ result += "• Determine primary win condition (team fights, split push, pick potential)\n\n"
821
+
822
+ result += "💪 **Strengths to Leverage:**\n"
823
+ result += "• Team fight potential\n"
824
+ result += "• Engage/disengage capability\n"
825
+ result += "• Damage type balance (AP/AD/True)\n"
826
+ result += "• Tankiness and peel for carries\n\n"
827
+
828
+ result += "⚠️ **Weaknesses to Cover:**\n"
829
+ result += "• Lack of engage or disengage\n"
830
+ result += "• Vulnerability to certain damage types\n"
831
+ result += "• Poor scaling or weak early game\n"
832
+ result += "• Limited crowd control\n\n"
833
+
834
+ result += "📋 **Game Plan:**\n"
835
+ if enemy_team:
836
+ result += "• Compare power spikes with enemy team\n"
837
+ result += "• Identify favorable and unfavorable matchups\n"
838
+ result += "• Play to your strengths and cover weaknesses\n"
839
+ result += "• Coordinate objectives around your win condition"
840
+
841
+ return result
842
+ except Exception as e:
843
+ logger.error(f"Error analyzing team composition: {e}")
844
+ return f"❌ Error: {str(e)}"
845
+
846
+ pregame_tools.append(analyze_team_composition)
847
+
848
+ @tool
849
+ def get_match_team_details(summoner_name: str = None, match_number: int = 1) -> str:
850
+ """
851
+ Get complete team composition details from a recent match, including all 10 players,
852
+ their champions, roles, and team assignments. Perfect for analyzing actual team comps.
853
+
854
+ Args:
855
+ summoner_name: Summoner name to get match history from (uses primary summoner if not provided)
856
+ match_number: Which recent match to analyze (1 = most recent, 2 = second most recent, etc.)
857
+ """
858
+ try:
859
+ # Determine which summoner to use
860
+ name = summoner_name or self.summoner_name
861
+ if not name:
862
+ return "❌ No summoner name provided and no default summoner configured."
863
+
864
+ tagline = self.get_tagline_for_summoner(name)
865
+ logger.info(f"Fetching match team details for {name}#{tagline}, match #{match_number}")
866
+
867
+ # Get summoner info
868
+ summoner_info = self.riot_api.get_summoner(name, tagline)
869
+ if not summoner_info or 'puuid' not in summoner_info:
870
+ return f"❌ Could not find summoner: {name}#{tagline}"
871
+
872
+ puuid = summoner_info['puuid']
873
+
874
+ # Get match history
875
+ matches = self.riot_api.get_match_history(puuid, count=max(match_number, 5))
876
+ if not matches or len(matches) < match_number:
877
+ return f"❌ Could not retrieve match #{match_number}. Only {len(matches) if matches else 0} matches available."
878
+
879
+ # Get the specific match
880
+ match_id = matches[match_number - 1]
881
+ logger.info(f"Analyzing match: {match_id}")
882
+
883
+ # Get full match data using Riot API (without puuid to get all participants)
884
+ match_data = self.riot_api.get_match_details(match_id)
885
+ if not match_data or 'info' not in match_data:
886
+ return f"❌ Could not retrieve match data for match #{match_number}"
887
+
888
+ info = match_data['info']
889
+ participants = info.get('participants', [])
890
+
891
+ if not participants:
892
+ return "❌ No participant data available for this match."
893
+
894
+ # Find the summoner's team
895
+ summoner_team_id = None
896
+ for p in participants:
897
+ if p.get('puuid') == puuid:
898
+ summoner_team_id = p.get('teamId')
899
+ break
900
+
901
+ # Organize teams
902
+ blue_team = []
903
+ red_team = []
904
+
905
+ for p in participants:
906
+ player_info = {
907
+ 'name': f"{p.get('riotIdGameName', 'Unknown')}#{p.get('riotIdTagline', '')}",
908
+ 'champion': p.get('championName', 'Unknown'),
909
+ 'role': p.get('teamPosition', 'UNKNOWN').replace('UTILITY', 'SUPPORT'),
910
+ 'kills': p.get('kills', 0),
911
+ 'deaths': p.get('deaths', 0),
912
+ 'assists': p.get('assists', 0),
913
+ 'win': p.get('win', False)
914
+ }
915
+
916
+ if p.get('teamId') == 100: # Blue side
917
+ blue_team.append(player_info)
918
+ else: # Red side
919
+ red_team.append(player_info)
920
+
921
+ # Sort teams by role
922
+ role_order = {'TOP': 0, 'JUNGLE': 1, 'MIDDLE': 2, 'BOTTOM': 3, 'SUPPORT': 4, 'UNKNOWN': 5}
923
+ blue_team.sort(key=lambda x: role_order.get(x['role'], 5))
924
+ red_team.sort(key=lambda x: role_order.get(x['role'], 5))
925
+
926
+ # Determine which team was yours
927
+ your_team_label = "Blue Team" if summoner_team_id == 100 else "Red Team"
928
+ enemy_team_label = "Red Team" if summoner_team_id == 100 else "Blue Team"
929
+ your_team = blue_team if summoner_team_id == 100 else red_team
930
+ enemy_team = red_team if summoner_team_id == 100 else blue_team
931
+
932
+ match_result = "Victory" if your_team[0]['win'] else "Defeat"
933
+
934
+ # Build result
935
+ result = f"🎮 **Match Team Composition - Match #{match_number}**\n\n"
936
+ result += f"**Match ID:** {match_id}\n"
937
+ result += f"**Result:** {match_result}\n"
938
+ result += f"**Summoner:** {name}#{tagline}\n\n"
939
+
940
+ result += f"═══════════════════════════════════\n"
941
+ result += f"**{your_team_label} (Your Team)** {'✅' if your_team[0]['win'] else '❌'}\n"
942
+ result += f"═══════════════════════════════════\n"
943
+ for player in your_team:
944
+ kda = f"{player['kills']}/{player['deaths']}/{player['assists']}"
945
+ role_icon = {'TOP': '⬆️', 'JUNGLE': '🌲', 'MIDDLE': '⭐', 'BOTTOM': '🎯', 'SUPPORT': '🛡️'}.get(player['role'], '❓')
946
+ result += f"{role_icon} **{player['role']}**: {player['champion']}\n"
947
+ result += f" Player: {player['name']} | KDA: {kda}\n"
948
+
949
+ result += f"\n═══════════════════════════════════\n"
950
+ result += f"**{enemy_team_label} (Enemy Team)** {'✅' if enemy_team[0]['win'] else '❌'}\n"
951
+ result += f"═══════════════════════════════════\n"
952
+ for player in enemy_team:
953
+ kda = f"{player['kills']}/{player['deaths']}/{player['assists']}"
954
+ role_icon = {'TOP': '⬆️', 'JUNGLE': '🌲', 'MIDDLE': '⭐', 'BOTTOM': '🎯', 'SUPPORT': '🛡️'}.get(player['role'], '❓')
955
+ result += f"{role_icon} **{player['role']}**: {player['champion']}\n"
956
+ result += f" Player: {player['name']} | KDA: {kda}\n"
957
+
958
+ # Create composition strings for further analysis
959
+ your_champions = [p['champion'] for p in your_team]
960
+ enemy_champions = [p['champion'] for p in enemy_team]
961
+
962
+ result += f"\n\n💡 **Team Composition Summary:**\n"
963
+ result += f"**Your Team:** {', '.join(your_champions)}\n"
964
+ result += f"**Enemy Team:** {', '.join(enemy_champions)}\n\n"
965
+ result += f"📊 You can now use `analyze_team_composition` with these exact teams for detailed strategic analysis!"
966
+
967
+ return result
968
+
969
+ except Exception as e:
970
+ logger.error(f"Error getting match team details: {e}")
971
+ return f"❌ Error retrieving match details: {str(e)}"
972
+
973
+ pregame_tools.append(get_match_team_details)
974
+
975
+ return {
976
+ "match": match_tools, # get_summoner_profile, analyze_recent_matches
977
+ "build": build_tools, # get_optimal_build, get_champion_matchups, get_personalized_build_advice
978
+ "video": video_tools, # find_champion_guides, find_matchup_videos, find_educational_videos
979
+ "knowledge": knowledge_tools, # search_lol_knowledge with FAISS, search_web_lol_info
980
+ "pregame": pregame_tools # recommend_bans, suggest_champion_pick, analyze_team_composition
981
+ }
982
+
983
+ def chat(self, user_message: str, thread_id: str = "default") -> str:
984
+ """
985
+ Handle a user message through the multi-agent system.
986
+
987
+ Args:
988
+ user_message: User's question or request
989
+ thread_id: Conversation thread ID
990
+
991
+ Returns:
992
+ Response from the appropriate agent(s)
993
+ """
994
+ logger.info(f"Processing user query: {user_message[:100]}...")
995
+ logger.info(f"Thread ID: {thread_id}")
996
+
997
+ print("\n" + "=" * 80)
998
+ print(f"💬 User: {user_message}")
999
+ print("=" * 80)
1000
+
1001
+ try:
1002
+ # 1. Route the query
1003
+ route = self.router.route(user_message)
1004
+ logger.info(f"Query routed to: {route.agent}")
1005
+
1006
+ # 2. Handle based on routing decision
1007
+ if route.agent == "orchestrator" or route.needs_multiple_agents:
1008
+ # Use orchestrator for complex queries
1009
+ logger.info("Using orchestrator for multi-agent workflow")
1010
+ response = self.orchestrator.handle_query(user_message, thread_id)
1011
+ else:
1012
+ # Direct to specific agent
1013
+ agent = self.agents.get(route.agent)
1014
+ if agent:
1015
+ agent_desc = self.router.get_agent_description(route.agent)
1016
+ print(f"\n{agent_desc}")
1017
+ logger.info(f"Invoking {route.agent}")
1018
+ response = agent.invoke(user_message, thread_id)
1019
+ logger.info(f"Agent {route.agent} completed successfully")
1020
+ else:
1021
+ error_msg = f"Agent '{route.agent}' not found"
1022
+ logger.error(error_msg)
1023
+ response = f"❌ {error_msg}"
1024
+
1025
+ except Exception as e:
1026
+ logger.error(f"Error processing query: {str(e)}", exc_info=True)
1027
+ response = self._handle_error(e, user_message)
1028
+
1029
+ print("\n" + "=" * 80)
1030
+ print("🤖 Response:")
1031
+ print(response)
1032
+ print("=" * 80 + "\n")
1033
+
1034
+ logger.info(f"Response length: {len(response)} characters")
1035
+ return response
1036
+
1037
+ def _handle_error(self, error: Exception, query: str) -> str:
1038
+ """
1039
+ Handle errors gracefully with fallback responses.
1040
+
1041
+ Args:
1042
+ error: The exception that occurred
1043
+ query: The original user query
1044
+
1045
+ Returns:
1046
+ User-friendly error message
1047
+ """
1048
+ error_type = type(error).__name__
1049
+ logger.error(f"Error type: {error_type}, Query: {query[:100]}")
1050
+
1051
+ # Check for specific error types
1052
+ if "API" in str(error) or "api" in str(error).lower():
1053
+ return ("⚠️ I'm having trouble connecting to the game data services right now. "
1054
+ "Please check your API keys and try again in a moment.")
1055
+
1056
+ if "rate limit" in str(error).lower():
1057
+ return ("⚠️ We've hit a rate limit. Please wait a moment and try again.")
1058
+
1059
+ if "timeout" in str(error).lower():
1060
+ return ("⚠️ The request took too long. Please try again with a simpler question.")
1061
+
1062
+ # Generic fallback
1063
+ return (f"❌ I encountered an error: {str(error)}\n\n"
1064
+ f"💡 Try asking about:\n"
1065
+ f" • Match analysis: 'Analyze my recent games'\n"
1066
+ f" • Champion builds: 'What items should I build on [champion]?'\n"
1067
+ f" • Video guides: 'Find guides for [champion]'\n"
1068
+ f" • Game knowledge: 'What does [term] mean?'\n"
1069
+ f" • Pre-game strategy: 'Who should I ban?'")
1070
+
1071
+ def _fallback_response(self, query: str) -> str:
1072
+ """
1073
+ Fallback response when routing fails or query is out of scope.
1074
+
1075
+ Args:
1076
+ query: The user's query
1077
+
1078
+ Returns:
1079
+ Helpful fallback message
1080
+ """
1081
+ logger.warning(f"Fallback triggered for query: {query[:100]}")
1082
+
1083
+ return ("🤔 I'm not quite sure how to help with that specific question.\n\n"
1084
+ "I specialize in:\n"
1085
+ " 🎯 **Match Analysis** - Review your recent games and performance\n"
1086
+ " 🛠️ **Build Advice** - Optimal items and runes for champions\n"
1087
+ " 🎬 **Video Guides** - Find tutorials and gameplay videos\n"
1088
+ " 📚 **Game Knowledge** - Explain League of Legends concepts\n"
1089
+ " 🎯 **Pre-game Strategy** - Champion select, bans, and drafting\n\n"
1090
+ "Try rephrasing your question or ask about one of these topics!")
1091
+
1092
+ def get_system_info(self) -> dict:
1093
+ """Get information about the multi-agent system."""
1094
+ logger.debug("Retrieving system information")
1095
+ return {
1096
+ "agents": {
1097
+ name: agent.get_info()
1098
+ for name, agent in self.agents.items()
1099
+ },
1100
+ "router": "Active",
1101
+ "orchestrator": "Active"
1102
+ }
1103
+
1104
+
1105
+ def create_multi_agent_coach(
1106
+ openai_api_key: str = None,
1107
+ riot_api_key: str = None,
1108
+ region: str = None,
1109
+ routing_value: str = None,
1110
+ summoner_name: str = None,
1111
+ summoner_tag: str = None,
1112
+ additional_summoners: str = None
1113
+ ) -> MultiAgentLoLCoach:
1114
+ """
1115
+ Create a configured multi-agent LoL coach system.
1116
+
1117
+ Args:
1118
+ openai_api_key: OpenAI API key (loads from .env if not provided)
1119
+ riot_api_key: Riot Games API key (loads from .env if not provided)
1120
+ region: Riot API region (loads from .env if not provided)
1121
+ routing_value: Riot API routing value (loads from .env if not provided)
1122
+ summoner_name: Primary summoner name (loads from .env if not provided)
1123
+ summoner_tag: Primary summoner tag (loads from .env if not provided)
1124
+ additional_summoners: Comma-separated list of summoners (loads from .env if not provided)
1125
+
1126
+ Returns:
1127
+ Configured MultiAgentLoLCoach instance
1128
+ """
1129
+ load_dotenv()
1130
+
1131
+ openai_api_key = openai_api_key or os.getenv("OPENAI_API_KEY")
1132
+ riot_api_key = riot_api_key or os.getenv("RIOT_API_KEY")
1133
+ region = region or os.getenv("REGION", "na1")
1134
+ routing_value = routing_value or os.getenv("ROUTING_VALUE", "americas")
1135
+ summoner_name = summoner_name or os.getenv("SUMMONER_NAME")
1136
+ summoner_tag = summoner_tag or os.getenv("SUMMONER_TAG")
1137
+
1138
+ # Parse additional summoners from comma-separated string
1139
+ additional_summoners_str = additional_summoners or os.getenv("ADDITIONAL_SUMMONERS", "")
1140
+ additional_summoners_list = [s.strip() for s in additional_summoners_str.split(",") if s.strip()]
1141
+
1142
+ return MultiAgentLoLCoach(
1143
+ openai_api_key=openai_api_key,
1144
+ riot_api_key=riot_api_key,
1145
+ region=region,
1146
+ routing_value=routing_value,
1147
+ summoner_name=summoner_name,
1148
+ summoner_tag=summoner_tag,
1149
+ additional_summoners=additional_summoners_list
1150
+ )
1151
+
1152
+
1153
+ def create_gradio_interface(coach: MultiAgentLoLCoach):
1154
+ """
1155
+ Create a Gradio web interface for the multi-agent coach.
1156
+
1157
+ Args:
1158
+ coach: Configured MultiAgentLoLCoach instance
1159
+
1160
+ Returns:
1161
+ Gradio Blocks interface
1162
+ """
1163
+ import gradio as gr
1164
+
1165
+ def chat_wrapper(message, history):
1166
+ """Wrapper for Gradio chat interface"""
1167
+ try:
1168
+ # Use message directly for processing
1169
+ response = coach.chat(message, "gradio_session")
1170
+ return response
1171
+ except Exception as e:
1172
+ logger.error(f"Gradio chat error: {str(e)}", exc_info=True)
1173
+ return f"❌ Error: {str(e)}\n\nPlease try again or rephrase your question."
1174
+
1175
+ # Create Gradio interface
1176
+ with gr.Blocks(title="⚔️ LoL Multi-Agent Coach") as demo:
1177
+ gr.Markdown(
1178
+ """
1179
+ # ⚔️ League of Legends Multi-Agent Coach
1180
+ ### AI-Powered Coaching with 5 Specialized Agents
1181
+
1182
+ **Available Agents:**
1183
+ - 🎯 **Pregame Agent** - Champion select, bans, draft strategy
1184
+ - 🎯 **Match Analyzer** - Game history and performance analysis
1185
+ - 🛠️ **Build Advisor** - Optimal items, runes, and champions
1186
+ - 🎬 **Video Guide** - YouTube tutorials and gameplay videos
1187
+ - 📚 **Knowledge Base** - Game concepts and terminology
1188
+
1189
+ *The system automatically routes your question to the best agent(s)!*
1190
+ """
1191
+ )
1192
+
1193
+ with gr.Row():
1194
+ with gr.Column(scale=2):
1195
+ chatbot = gr.Chatbot(
1196
+ height=500,
1197
+ label="Multi-Agent Coach Chat",
1198
+ show_label=True
1199
+ )
1200
+ msg = gr.Textbox(
1201
+ label="Your Question",
1202
+ placeholder="E.g., 'Who should I ban?' or 'What items should I build on Ahri?'",
1203
+ lines=2
1204
+ )
1205
+
1206
+ with gr.Row():
1207
+ submit = gr.Button("Send", variant="primary", size="lg")
1208
+ clear = gr.Button("Clear Chat", size="lg")
1209
+
1210
+ with gr.Column(scale=1):
1211
+ gr.Markdown("### 💡 Example Questions")
1212
+ examples = gr.Examples(
1213
+ examples=[
1214
+ "Who should I ban in ranked?",
1215
+ "What champion should I pick for mid lane?",
1216
+ "Analyze my recent matches",
1217
+ "What items should I build on Ahri?",
1218
+ "Find Yasuo guides",
1219
+ "What does AP mean?",
1220
+ "I keep losing as Jinx, help me",
1221
+ "What are good team compositions?",
1222
+ "How do I counter Yasuo?",
1223
+ "Show me educational videos about wave management"
1224
+ ],
1225
+ inputs=msg,
1226
+ label="Click to try:"
1227
+ )
1228
+
1229
+ gr.Markdown(
1230
+ """
1231
+ ### 🎯 Routing Intelligence
1232
+
1233
+ The router automatically detects:
1234
+ - **Pre-game questions** → Pregame Agent
1235
+ - **Performance questions** → Match Analyzer
1236
+ - **Build questions** → Build Advisor
1237
+ - **Video requests** → Video Guide
1238
+ - **Learning questions** → Knowledge Base
1239
+ - **Complex questions** → Multi-agent orchestration
1240
+ """
1241
+ )
1242
+
1243
+ with gr.Accordion("📊 System Information", open=False):
1244
+ sys_info = coach.get_system_info()
1245
+ gr.JSON(value=sys_info, label="Active Agents")
1246
+
1247
+ # Event handlers - Gradio 6.x format with role/content dictionaries
1248
+ def respond(message, history):
1249
+ """Handle chat response with proper Gradio 6.x format"""
1250
+ if not message or not message.strip():
1251
+ return history, ""
1252
+
1253
+ bot_response = chat_wrapper(message, history)
1254
+
1255
+ # Gradio 6.x expects list of dicts with 'role' and 'content'
1256
+ history = history or []
1257
+ history.append({"role": "user", "content": message})
1258
+ history.append({"role": "assistant", "content": bot_response})
1259
+
1260
+ return history, ""
1261
+
1262
+ msg.submit(respond, [msg, chatbot], [chatbot, msg])
1263
+ submit.click(respond, [msg, chatbot], [chatbot, msg])
1264
+ clear.click(lambda: [], None, chatbot, queue=False)
1265
+
1266
+ return demo
1267
+
1268
+
1269
+ # Example usage
1270
+ if __name__ == "__main__":
1271
+ import sys
1272
+
1273
+ # Create the multi-agent system
1274
+ coach = create_multi_agent_coach()
1275
+
1276
+ # Check if running with --ui flag
1277
+ if "--ui" in sys.argv or "-ui" in sys.argv:
1278
+ logger.info("Starting Gradio UI interface...")
1279
+ print("\n🚀 Launching Gradio Web Interface...")
1280
+ demo = create_gradio_interface(coach)
1281
+ demo.launch(
1282
+ share=True,
1283
+ server_name="127.0.0.1",
1284
+ server_port=7860
1285
+ )
1286
+ else:
1287
+ # CLI test mode
1288
+ # Test queries demonstrating different routing
1289
+ test_queries = [
1290
+ "Who should I ban in ranked?", # → pregame_agent
1291
+ "Analyze my recent matches", # → match_analyzer
1292
+ "What items should I build on Ahri?", # → build_advisor
1293
+ "Find Yasuo guides", # → video_guide
1294
+ "What does AP mean?", # → knowledge_base
1295
+ "I keep losing as Jinx, help me get better", # → orchestrator (multiple agents)
1296
+ ]
1297
+
1298
+ print("\n" + "🧪" * 40)
1299
+ print("TESTING MULTI-AGENT SYSTEM")
1300
+ print("🧪" * 40 + "\n")
1301
+
1302
+ for query in test_queries:
1303
+ coach.chat(query)
1304
+ print("\n")
1305
+
1306
+ # Display system info
1307
+ print("\n" + "ℹ️" * 40)
1308
+ print("SYSTEM INFORMATION")
1309
+ print("ℹ️" * 40)
1310
+ import json
1311
+ print(json.dumps(coach.get_system_info(), indent=2))
1312
+
1313
+ print("\n💡 Tip: Run with '--ui' flag to launch web interface:")
1314
+ print(" python multi_agent_coach.py --ui")
1315
+
push.sh ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Quick push script for Hugging Face Space
4
+
5
+ echo "🚀 Pushing LoL Multi-Agent Coach to Hugging Face..."
6
+ echo ""
7
+
8
+ cd /Users/ralitzamondal/Documents/LolMultiAgent
9
+
10
+ # Check git status
11
+ echo "📋 Current status:"
12
+ git status
13
+ echo ""
14
+
15
+ # Ask user to confirm
16
+ read -p "Ready to push? (y/n) " -n 1 -r
17
+ echo ""
18
+
19
+ if [[ $REPLY =~ ^[Yy]$ ]]
20
+ then
21
+ echo "🔐 Pushing to Hugging Face..."
22
+ echo " (You may need to enter your HF token)"
23
+ git push
24
+
25
+ if [ $? -eq 0 ]; then
26
+ echo ""
27
+ echo "✅ SUCCESS! Your Space is updating!"
28
+ echo "🌐 Visit: https://huggingface.co/spaces/Ralitza1/LolMultiAgent"
29
+ echo ""
30
+ echo "⏳ Wait 5-10 minutes for build to complete"
31
+ echo "🔑 Don't forget to add API keys in Space Settings!"
32
+ else
33
+ echo ""
34
+ echo "❌ Push failed. You may need to:"
35
+ echo " 1. Get your HF token: https://huggingface.co/settings/tokens"
36
+ echo " 2. Run: huggingface-cli login"
37
+ echo " 3. Try again"
38
+ fi
39
+ else
40
+ echo "❌ Push cancelled"
41
+ fi
requirements.txt ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==24.1.0
2
+ aiohappyeyeballs==2.6.1
3
+ aiohttp==3.13.2
4
+ aiosignal==1.4.0
5
+ annotated-doc==0.0.4
6
+ annotated-types==0.7.0
7
+ anyio==4.12.0
8
+ attrs==25.4.0
9
+ audioop-lts==0.2.2
10
+ beautifulsoup4==4.14.3
11
+ brotli==1.2.0
12
+ cachetools==6.2.4
13
+ certifi==2025.11.12
14
+ charset-normalizer==3.4.4
15
+ click==8.3.1
16
+ dataclasses-json==0.6.7
17
+ distro==1.9.0
18
+ faiss-cpu==1.13.1
19
+ fastapi==0.127.0
20
+ ffmpy==1.0.0
21
+ filelock==3.20.1
22
+ frozenlist==1.8.0
23
+ fsspec==2025.12.0
24
+ google-api-core==2.28.1
25
+ google-api-python-client==2.187.0
26
+ google-auth==2.41.1
27
+ google-auth-httplib2==0.3.0
28
+ google-auth-oauthlib==1.2.3
29
+ googleapis-common-protos==1.72.0
30
+ gradio==6.2.0
31
+ gradio_client==2.0.2
32
+ groovy==0.1.2
33
+ h11==0.16.0
34
+ hf-xet==1.2.0
35
+ httpcore==1.0.9
36
+ httplib2==0.31.0
37
+ httpx==0.28.1
38
+ httpx-sse==0.4.3
39
+ huggingface_hub==1.2.3
40
+ idna==3.11
41
+ iniconfig==2.3.0
42
+ Jinja2==3.1.6
43
+ jiter==0.12.0
44
+ jsonpatch==1.33
45
+ jsonpointer==3.0.0
46
+ langchain==1.2.0
47
+ langchain-classic==1.0.0
48
+ langchain-community==0.4.1
49
+ langchain-core==1.2.4
50
+ langchain-openai==1.1.6
51
+ langchain-text-splitters==1.1.0
52
+ langgraph==1.0.5
53
+ langgraph-checkpoint==3.0.1
54
+ langgraph-prebuilt==1.0.5
55
+ langgraph-sdk==0.3.1
56
+ langsmith==0.5.0
57
+ lxml==6.0.2
58
+ markdown-it-py==4.0.0
59
+ MarkupSafe==3.0.3
60
+ marshmallow==3.26.1
61
+ mdurl==0.1.2
62
+ multidict==6.7.0
63
+ mypy_extensions==1.1.0
64
+ numpy==2.4.0
65
+ oauthlib==3.3.1
66
+ openai==2.14.0
67
+ orjson==3.11.5
68
+ ormsgpack==1.12.1
69
+ outcome==1.3.0.post0
70
+ packaging==25.0
71
+ pandas==2.3.3
72
+ pillow==12.0.0
73
+ pluggy==1.6.0
74
+ propcache==0.4.1
75
+ proto-plus==1.27.0
76
+ protobuf==6.33.2
77
+ pyasn1==0.6.1
78
+ pyasn1_modules==0.4.2
79
+ pydantic==2.12.5
80
+ pydantic-settings==2.12.0
81
+ pydantic_core==2.41.5
82
+ pydub==0.25.1
83
+ Pygments==2.19.2
84
+ pyparsing==3.2.5
85
+ PySocks==1.7.1
86
+ pytest==9.0.2
87
+ python-dateutil==2.9.0.post0
88
+ python-dotenv==1.2.1
89
+ python-multipart==0.0.21
90
+ pytz==2025.2
91
+ PyYAML==6.0.3
92
+ regex==2025.11.3
93
+ requests==2.32.5
94
+ requests-oauthlib==2.0.0
95
+ requests-toolbelt==1.0.0
96
+ rich==14.2.0
97
+ rsa==4.9.1
98
+ safehttpx==0.1.7
99
+ selenium==4.39.0
100
+ semantic-version==2.10.0
101
+ shellingham==1.5.4
102
+ six==1.17.0
103
+ sniffio==1.3.1
104
+ sortedcontainers==2.4.0
105
+ soupsieve==2.8.1
106
+ SQLAlchemy==2.0.45
107
+ starlette==0.50.0
108
+ tavily-python==0.7.19
109
+ tenacity==9.1.2
110
+ tiktoken==0.12.0
111
+ tomlkit==0.13.3
112
+ tqdm==4.67.1
113
+ trio==0.32.0
114
+ trio-websocket==0.12.2
115
+ typer==0.20.1
116
+ typer-slim==0.20.1
117
+ typing-inspect==0.9.0
118
+ typing-inspection==0.4.2
119
+ typing_extensions==4.15.0
120
+ tzdata==2025.3
121
+ uritemplate==4.2.0
122
+ urllib3==2.6.2
123
+ uuid_utils==0.12.0
124
+ uvicorn==0.40.0
125
+ websocket-client==1.9.0
126
+ wsproto==1.3.2
127
+ xxhash==3.6.0
128
+ yarl==1.22.0
129
+ zstandard==0.25.0
riot_api.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Riot Games API Integration
3
+
4
+ Handles all interactions with the Riot Games API for fetching summoner data,
5
+ match history, and game statistics.
6
+ """
7
+
8
+ import requests
9
+ import time
10
+ from typing import Dict, List, Optional
11
+
12
+
13
+ class RiotAPI:
14
+ """Wrapper for Riot Games API calls"""
15
+
16
+ def __init__(self, api_key: str, region: str = "na1", routing: str = "americas"):
17
+ """
18
+ Initialize Riot API client
19
+
20
+ Args:
21
+ api_key: Riot Games API key from developer.riotgames.com
22
+ region: Platform routing (na1, euw1, kr, etc.)
23
+ routing: Regional routing (americas, europe, asia, sea)
24
+ """
25
+ self.api_key = api_key
26
+ self.region = region
27
+ self.routing = routing
28
+ self.base_url = f"https://{region}.api.riotgames.com"
29
+ self.routing_url = f"https://{routing}.api.riotgames.com"
30
+ self.headers = {"X-Riot-Token": api_key}
31
+
32
+ # Rate limiting
33
+ self.last_request_time = 0
34
+ self.min_request_interval = 0.05 # 50ms between requests
35
+
36
+ def _rate_limit(self):
37
+ """Implement basic rate limiting"""
38
+ current_time = time.time()
39
+ time_since_last = current_time - self.last_request_time
40
+ if time_since_last < self.min_request_interval:
41
+ time.sleep(self.min_request_interval - time_since_last)
42
+ self.last_request_time = time.time()
43
+
44
+ def _make_request(self, url: str) -> Optional[Dict]:
45
+ """Make API request with error handling"""
46
+ self._rate_limit()
47
+
48
+ try:
49
+ response = requests.get(url, headers=self.headers, timeout=10)
50
+
51
+ if response.status_code == 200:
52
+ return response.json()
53
+ elif response.status_code == 404:
54
+ print(f"❌ Not found: {url}")
55
+ return None
56
+ elif response.status_code == 429:
57
+ print("⚠️ Rate limit exceeded. Waiting...")
58
+ time.sleep(2)
59
+ return self._make_request(url)
60
+ elif response.status_code == 403:
61
+ print("❌ Invalid API key or expired")
62
+ return None
63
+ else:
64
+ print(f"❌ API Error {response.status_code}: {response.text}")
65
+ return None
66
+ except requests.exceptions.RequestException as e:
67
+ print(f"❌ Request failed: {str(e)}")
68
+ return None
69
+
70
+ def get_account_by_riot_id(self, game_name: str, tag_line: str) -> Optional[Dict]:
71
+ """
72
+ Get account information by Riot ID (game name + tag line)
73
+ This is the new Riot ID system that replaced summoner names
74
+
75
+ Args:
76
+ game_name: The game name (e.g., "BeLikeThatOrElse")
77
+ tag_line: The tag line (e.g., "NA1")
78
+
79
+ Returns:
80
+ Dict with keys: puuid, gameName, tagLine
81
+ """
82
+ url = f"{self.routing_url}/riot/account/v1/accounts/by-riot-id/{game_name}/{tag_line}"
83
+ return self._make_request(url)
84
+
85
+ def get_summoner_by_puuid(self, puuid: str) -> Optional[Dict]:
86
+ """
87
+ Get summoner information by PUUID
88
+
89
+ Returns:
90
+ Dict with keys: id, accountId, puuid, name, summonerLevel, etc.
91
+ """
92
+ url = f"{self.base_url}/lol/summoner/v4/summoners/by-puuid/{puuid}"
93
+ return self._make_request(url)
94
+
95
+ def get_summoner_by_name(self, summoner_name: str) -> Optional[Dict]:
96
+ """
97
+ Get summoner information by summoner name (DEPRECATED - use Riot ID instead)
98
+
99
+ Returns:
100
+ Dict with keys: id, accountId, puuid, name, summonerLevel, etc.
101
+ """
102
+ url = f"{self.base_url}/lol/summoner/v4/summoners/by-name/{summoner_name}"
103
+ return self._make_request(url)
104
+
105
+ def get_ranked_stats(self, summoner_id: str) -> Optional[List[Dict]]:
106
+ """
107
+ Get ranked statistics for a summoner
108
+
109
+ Returns:
110
+ List of ranked queue info (Solo/Duo, Flex, etc.)
111
+ """
112
+ url = f"{self.base_url}/lol/league/v4/entries/by-summoner/{summoner_id}"
113
+ return self._make_request(url)
114
+
115
+ def get_match_history(self, puuid: str, count: int = 20, queue: int = None) -> Optional[List[str]]:
116
+ """
117
+ Get match history IDs for a summoner
118
+
119
+ Args:
120
+ puuid: Player UUID
121
+ count: Number of matches to retrieve (max 100)
122
+ queue: Queue ID filter (420=Ranked Solo, 400=Normal Draft, etc.)
123
+
124
+ Returns:
125
+ List of match IDs
126
+ """
127
+ url = f"{self.routing_url}/lol/match/v5/matches/by-puuid/{puuid}/ids?count={count}"
128
+ if queue:
129
+ url += f"&queue={queue}"
130
+ return self._make_request(url)
131
+
132
+ def get_match_details(self, match_id: str, puuid: str = None) -> Optional[Dict]:
133
+ """
134
+ Get detailed information about a match
135
+
136
+ Args:
137
+ match_id: Match ID
138
+ puuid: If provided, returns only this player's stats
139
+
140
+ Returns:
141
+ Match details or player-specific stats if puuid provided
142
+ """
143
+ url = f"{self.routing_url}/lol/match/v5/matches/{match_id}"
144
+ match_data = self._make_request(url)
145
+
146
+ if not match_data:
147
+ return None
148
+
149
+ # If puuid specified, extract only that player's data
150
+ if puuid:
151
+ participants = match_data.get('info', {}).get('participants', [])
152
+ for participant in participants:
153
+ if participant.get('puuid') == puuid:
154
+ return self._format_player_stats(participant)
155
+ return None
156
+
157
+ return match_data
158
+
159
+ def get_match_with_enemies(self, match_id: str, puuid: str) -> Optional[Dict]:
160
+ """
161
+ Get match details including enemy team information
162
+
163
+ Args:
164
+ match_id: Match ID
165
+ puuid: Player's UUID to identify their team
166
+
167
+ Returns:
168
+ Dict with player stats and enemy team data
169
+ """
170
+ url = f"{self.routing_url}/lol/match/v5/matches/{match_id}"
171
+ match_data = self._make_request(url)
172
+
173
+ if not match_data:
174
+ return None
175
+
176
+ participants = match_data.get('info', {}).get('participants', [])
177
+
178
+ # Find player and their team
179
+ player_data = None
180
+ player_team = None
181
+
182
+ for participant in participants:
183
+ if participant.get('puuid') == puuid:
184
+ player_data = self._format_player_stats(participant)
185
+ player_team = participant.get('teamId')
186
+ break
187
+
188
+ if not player_data:
189
+ return None
190
+
191
+ # Get enemy team
192
+ enemies = []
193
+ for participant in participants:
194
+ if participant.get('teamId') != player_team:
195
+ enemies.append({
196
+ 'champion': participant.get('championName', 'Unknown'),
197
+ 'role': participant.get('teamPosition', 'Unknown'),
198
+ 'kills': participant.get('kills', 0),
199
+ 'deaths': participant.get('deaths', 0),
200
+ 'assists': participant.get('assists', 0),
201
+ })
202
+
203
+ return {
204
+ 'player': player_data,
205
+ 'enemies': enemies,
206
+ 'game_duration': match_data.get('info', {}).get('gameDuration', 0),
207
+ }
208
+
209
+ def _format_player_stats(self, participant: Dict) -> Dict:
210
+ """Format participant data into clean stats"""
211
+ return {
212
+ 'champion': participant.get('championName', 'Unknown'),
213
+ 'kills': participant.get('kills', 0),
214
+ 'deaths': participant.get('deaths', 0),
215
+ 'assists': participant.get('assists', 0),
216
+ 'cs': participant.get('totalMinionsKilled', 0) + participant.get('neutralMinionsKilled', 0),
217
+ 'damage': participant.get('totalDamageDealtToChampions', 0),
218
+ 'gold': participant.get('goldEarned', 0),
219
+ 'vision_score': participant.get('visionScore', 0),
220
+ 'win': participant.get('win', False),
221
+ 'items': [
222
+ self._get_item_name(participant.get(f'item{i}', 0))
223
+ for i in range(7)
224
+ ],
225
+ 'summoner_spells': [
226
+ participant.get('summoner1Id'),
227
+ participant.get('summoner2Id')
228
+ ],
229
+ 'role': participant.get('teamPosition', 'Unknown'),
230
+ 'game_duration': participant.get('gameDuration', 0),
231
+ }
232
+
233
+ def _get_item_name(self, item_id: int) -> str:
234
+ """Convert item ID to name (simplified, would need Data Dragon for full names)"""
235
+ if item_id == 0:
236
+ return "Empty"
237
+ return f"Item {item_id}"
238
+
239
+ def get_champion_mastery(self, puuid: str, champion_id: int = None) -> Optional[Dict]:
240
+ """
241
+ Get champion mastery information
242
+
243
+ Args:
244
+ puuid: Player UUID
245
+ champion_id: Specific champion ID, or None for all
246
+
247
+ Returns:
248
+ Champion mastery data
249
+ """
250
+ if champion_id:
251
+ url = f"{self.base_url}/lol/champion-mastery/v4/champion-masteries/by-puuid/{puuid}/by-champion/{champion_id}"
252
+ else:
253
+ url = f"{self.base_url}/lol/champion-mastery/v4/champion-masteries/by-puuid/{puuid}"
254
+ return self._make_request(url)
255
+
256
+ def get_current_game(self, summoner_id: str) -> Optional[Dict]:
257
+ """
258
+ Get information about a summoner's current active game
259
+
260
+ Returns:
261
+ Current game info or None if not in game
262
+ """
263
+ url = f"{self.base_url}/lol/spectator/v4/active-games/by-summoner/{summoner_id}"
264
+ return self._make_request(url)
265
+
266
+
267
+ # Utility functions
268
+
269
+ def calculate_kda(kills: int, deaths: int, assists: int) -> float:
270
+ """Calculate KDA ratio"""
271
+ if deaths == 0:
272
+ return float(kills + assists)
273
+ return (kills + assists) / deaths
274
+
275
+
276
+ def calculate_cs_per_min(cs: int, game_duration: int) -> float:
277
+ """Calculate CS per minute"""
278
+ if game_duration == 0:
279
+ return 0.0
280
+ minutes = game_duration / 60
281
+ return cs / minutes
282
+
283
+
284
+ def get_rank_tier(tier: str, division: str) -> int:
285
+ """Convert rank to numeric tier for comparison"""
286
+ tier_values = {
287
+ 'IRON': 0, 'BRONZE': 1, 'SILVER': 2, 'GOLD': 3,
288
+ 'PLATINUM': 4, 'EMERALD': 5, 'DIAMOND': 6,
289
+ 'MASTER': 7, 'GRANDMASTER': 8, 'CHALLENGER': 9
290
+ }
291
+ division_values = {'IV': 0, 'III': 1, 'II': 2, 'I': 3}
292
+
293
+ tier_value = tier_values.get(tier.upper(), 0) * 4
294
+ div_value = division_values.get(division, 0)
295
+
296
+ return tier_value + div_value
specialized_agents.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Specialized Agents for LoL Coach Multi-Agent System
3
+ Each agent is an expert in a specific domain with specialized tools.
4
+ """
5
+
6
+ import logging
7
+ from typing import List, Dict, Any
8
+ from langchain_openai import ChatOpenAI
9
+ from langchain.tools import tool
10
+ from langgraph.prebuilt import create_react_agent
11
+ from langgraph.checkpoint.memory import MemorySaver
12
+ from langchain_core.messages import SystemMessage
13
+
14
+ # Setup logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class BaseLoLAgent:
19
+ """Base class for all specialized LoL coaching agents."""
20
+
21
+ def __init__(self, llm: ChatOpenAI, tools: List, system_prompt: str, name: str):
22
+ self.name = name
23
+ self.llm = llm
24
+ self.tools = tools
25
+ self.system_prompt = system_prompt
26
+ self.memory = MemorySaver()
27
+ logger.info(f"Initializing {name} agent with {len(tools)} tools")
28
+ self.agent = create_react_agent(
29
+ llm,
30
+ tools,
31
+ checkpointer=self.memory
32
+ )
33
+
34
+ def invoke(self, query: str, thread_id: str = "default") -> str:
35
+ """Invoke the agent with a query."""
36
+ logger.debug(f"{self.name} invoked with query: {query[:100]}...")
37
+ config = {"configurable": {"thread_id": thread_id}}
38
+
39
+ try:
40
+ # Add system prompt as first message
41
+ messages = [
42
+ SystemMessage(content=self.system_prompt),
43
+ ("user", query)
44
+ ]
45
+
46
+ response = self.agent.invoke(
47
+ {"messages": messages},
48
+ config=config
49
+ )
50
+
51
+ # Extract the final response
52
+ messages = response.get("messages", [])
53
+ if messages:
54
+ result = messages[-1].content
55
+ logger.debug(f"{self.name} generated response of length {len(result)}")
56
+ return result
57
+
58
+ logger.warning(f"{self.name} generated no response")
59
+ return "No response generated."
60
+
61
+ except Exception as e:
62
+ logger.error(f"{self.name} invoke failed: {str(e)}", exc_info=True)
63
+ return f"Error: {str(e)}"
64
+
65
+ def get_info(self) -> Dict[str, Any]:
66
+ """Get agent information."""
67
+ return {
68
+ "name": self.name,
69
+ "tool_count": len(self.tools),
70
+ "tools": [tool.name for tool in self.tools]
71
+ }
72
+
73
+
74
+ class MatchAnalyzerAgent(BaseLoLAgent):
75
+ """
76
+ 🎯 Specialized agent for analyzing match history and performance.
77
+
78
+ Expertise:
79
+ - Match history analysis
80
+ - Performance statistics
81
+ - Win/loss tracking
82
+ - Enemy champion analysis
83
+ - Build comparisons from past games
84
+ """
85
+
86
+ def __init__(self, llm: ChatOpenAI, match_tools: List):
87
+ system_prompt = """You are the Match Analyzer Agent, an expert at analyzing League of Legends match history and player performance.
88
+
89
+ Your expertise:
90
+ - Analyzing recent match data and trends
91
+ - Identifying performance patterns
92
+ - Comparing player stats across games
93
+ - Analyzing matchups against specific enemy champions
94
+ - Reviewing build choices from past games
95
+
96
+ Your communication style:
97
+ - Data-driven and analytical
98
+ - Highlight trends and patterns
99
+ - Provide specific statistics
100
+ - Compare performance across games
101
+ - Offer actionable insights based on data
102
+
103
+ When analyzing matches:
104
+ 1. Look for trends across multiple games
105
+ 2. Identify what's working and what isn't
106
+ 3. Compare against enemy champions
107
+ 4. Note build effectiveness
108
+ 5. Provide clear, specific recommendations
109
+
110
+ Always base your analysis on actual match data and statistics."""
111
+
112
+ super().__init__(llm, match_tools, system_prompt, "Match Analyzer")
113
+
114
+
115
+ class BuildAdvisorAgent(BaseLoLAgent):
116
+ """
117
+ 🛠️ Specialized agent for build recommendations and champion advice.
118
+
119
+ Expertise:
120
+ - Optimal item builds
121
+ - Rune recommendations
122
+ - Champion matchups
123
+ - Counter picks
124
+ - Build optimization strategies
125
+ """
126
+
127
+ def __init__(self, llm: ChatOpenAI, build_tools: List):
128
+ system_prompt = """You are the Build Advisor Agent, an expert at recommending optimal builds, runes, and champions for League of Legends.
129
+
130
+ Your expertise:
131
+ - Optimal item builds for any champion and role
132
+ - Best rune pages and setups
133
+ - Champion matchup advice
134
+ - Counter picks and strategies
135
+ - Build adaptations for different situations
136
+
137
+ Your communication style:
138
+ - Clear and instructional
139
+ - Explain WHY builds work
140
+ - Provide situational alternatives
141
+ - Consider enemy team composition
142
+ - Offer practical, tested recommendations
143
+
144
+ When recommending builds:
145
+ 1. Consider the champion's playstyle
146
+ 2. Factor in enemy team composition
147
+ 3. Explain item synergies
148
+ 4. Suggest rune reasoning
149
+ 5. Provide build order and power spikes
150
+
151
+ Always use current meta builds from U.GG and explain your recommendations."""
152
+
153
+ super().__init__(llm, build_tools, system_prompt, "Build Advisor")
154
+
155
+
156
+ class VideoGuideAgent(BaseLoLAgent):
157
+ """
158
+ 🎬 Specialized agent for finding YouTube guides and educational content.
159
+
160
+ Expertise:
161
+ - YouTube video searches
162
+ - Champion guide recommendations
163
+ - Matchup-specific videos
164
+ - Educational tutorials
165
+ - Pro gameplay examples
166
+ """
167
+
168
+ def __init__(self, llm: ChatOpenAI, video_tools: List):
169
+ system_prompt = """You are the Video Guide Agent, an expert at finding the best YouTube videos and educational content for League of Legends players.
170
+
171
+ Your expertise:
172
+ - Finding high-quality champion guides
173
+ - Locating matchup-specific gameplay videos
174
+ - Discovering educational content on game concepts
175
+ - Recommending relevant tutorials
176
+ - Curating learning resources
177
+
178
+ Your communication style:
179
+ - Enthusiastic and encouraging
180
+ - Explain what to watch for in videos
181
+ - Highlight key learning points
182
+ - Suggest viewing order for multiple videos
183
+ - Make learning feel approachable
184
+
185
+ When recommending videos:
186
+ 1. Search for relevant, recent content
187
+ 2. Explain what players will learn
188
+ 3. Highlight key takeaways
189
+ 4. Suggest what to focus on while watching
190
+ 5. Connect videos to player's goals
191
+
192
+ Always provide video context and learning objectives."""
193
+
194
+ super().__init__(llm, video_tools, system_prompt, "Video Guide")
195
+
196
+
197
+ class KnowledgeAgent(BaseLoLAgent):
198
+ """
199
+ 📚 Specialized agent for explaining game concepts and terminology.
200
+
201
+ Expertise:
202
+ - Game mechanics explanations
203
+ - Terminology definitions
204
+ - Concept clarifications
205
+ - Strategy fundamentals
206
+ - General LoL knowledge
207
+ """
208
+
209
+ def __init__(self, llm: ChatOpenAI, knowledge_tools: List):
210
+ system_prompt = """You are the Knowledge Agent, an expert at explaining League of Legends concepts, terminology, and game mechanics.
211
+
212
+ Your expertise:
213
+ - Clear explanations of game terms (AP, AD, armor, etc.)
214
+ - Game mechanics and how they work
215
+ - Strategic concepts (wave management, vision control, etc.)
216
+ - Champion abilities and interactions
217
+ - General League of Legends knowledge
218
+
219
+ Your communication style:
220
+ - Patient and educational
221
+ - Use simple language first, then add depth
222
+ - Provide examples to illustrate concepts
223
+ - Connect concepts to practical gameplay
224
+ - Encourage learning and understanding
225
+
226
+ When explaining concepts:
227
+ 1. Start with a clear, simple definition
228
+ 2. Explain how it works in practice
229
+ 3. Provide concrete examples
230
+ 4. Connect to gameplay situations
231
+ 5. Suggest how to apply the knowledge
232
+
233
+ Always make complex concepts accessible and understandable."""
234
+
235
+ super().__init__(llm, knowledge_tools, system_prompt, "Knowledge Base")
236
+
237
+
238
+ class PregameAgent(BaseLoLAgent):
239
+ """
240
+ 🎯 Specialized agent for champion select and draft strategy.
241
+
242
+ Expertise:
243
+ - Champion select recommendations
244
+ - Ban strategy and meta analysis
245
+ - Team composition synergy
246
+ - Counter-picking advice
247
+ - Draft phase optimization
248
+ - Role assignment strategy
249
+ """
250
+
251
+ def __init__(self, llm: ChatOpenAI, pregame_tools: List):
252
+ system_prompt = """You are the Pregame Agent, an expert at champion select, drafting, and pre-game strategy.
253
+
254
+ Your expertise:
255
+ - Optimal ban recommendations based on current meta and counters
256
+ - Champion select strategy for each role
257
+ - Team composition analysis and champion synergy
258
+ - Counter-picking strategies against enemy champions
259
+ - Draft phase optimization and priority picking
260
+ - Role assignment and lane matchup evaluation
261
+ - Champion pool recommendations for climbing ranked
262
+
263
+ Your communication style:
264
+ - Strategic and tactical
265
+ - Consider both teams' compositions
266
+ - Explain pick/ban reasoning clearly
267
+ - Adapt advice to player's champion pool
268
+ - Focus on win conditions and team synergy
269
+ - Provide backup options and flexibility
270
+
271
+ When providing draft advice:
272
+ 1. Analyze enemy team composition if available
273
+ 2. Consider team synergy and win conditions
274
+ 3. Recommend champions based on role and matchup
275
+ 4. Explain counter-pick opportunities
276
+ 5. Suggest ban priorities (high-priority meta picks, hard counters)
277
+ 6. Provide multiple options with reasoning
278
+ 7. Consider player comfort vs optimal picks
279
+
280
+ For bans specifically:
281
+ - Prioritize overpowered meta champions
282
+ - Ban hard counters to your team's picks
283
+ - Consider enemy team's likely picks
284
+ - Ban champions that enable problematic team compositions
285
+
286
+ For champion select:
287
+ - Match champion to player's role and skill level
288
+ - Consider lane matchup and counter-picks
289
+ - Evaluate team synergy (engage, poke, scaling, etc.)
290
+ - Suggest champions that fit the team's win condition
291
+ - Provide safe blind picks vs strong counter-picks
292
+
293
+ Always prioritize strategic depth while being practical about player skill and champion pool."""
294
+
295
+ super().__init__(llm, pregame_tools, system_prompt, "Pregame Strategy")
296
+
297
+
298
+ def create_specialized_agents(
299
+ llm: ChatOpenAI,
300
+ all_tools: Dict[str, List]
301
+ ) -> Dict[str, BaseLoLAgent]:
302
+ """
303
+ Create all specialized agents with their respective tools.
304
+
305
+ Args:
306
+ llm: ChatOpenAI instance
307
+ all_tools: Dictionary mapping tool categories to tool lists
308
+ - "match": Match analysis tools
309
+ - "build": Build/champion advice tools
310
+ - "video": YouTube search tools
311
+ - "knowledge": Knowledge base tools
312
+ - "pregame": Pregame/draft strategy tools
313
+
314
+ Returns:
315
+ Dictionary mapping agent names to agent instances
316
+ """
317
+ agents = {
318
+ "match_analyzer": MatchAnalyzerAgent(
319
+ llm=llm,
320
+ match_tools=all_tools.get("match", [])
321
+ ),
322
+ "build_advisor": BuildAdvisorAgent(
323
+ llm=llm,
324
+ build_tools=all_tools.get("build", [])
325
+ ),
326
+ "video_guide": VideoGuideAgent(
327
+ llm=llm,
328
+ video_tools=all_tools.get("video", [])
329
+ ),
330
+ "knowledge_base": KnowledgeAgent(
331
+ llm=llm,
332
+ knowledge_tools=all_tools.get("knowledge", [])
333
+ ),
334
+ "pregame_agent": PregameAgent(
335
+ llm=llm,
336
+ pregame_tools=all_tools.get("pregame", [])
337
+ )
338
+ }
339
+
340
+ return agents
341
+
342
+
343
+ # Example agent info display
344
+ if __name__ == "__main__":
345
+ print("=" * 80)
346
+ print("🤖 LoL Coach - Specialized Agents")
347
+ print("=" * 80)
348
+
349
+ agent_descriptions = {
350
+ "Match Analyzer": "🎯 Analyzes match history, performance stats, and game trends",
351
+ "Build Advisor": "🛠️ Recommends optimal items, runes, and champion strategies",
352
+ "Video Guide": "🎬 Finds YouTube guides, tutorials, and educational content",
353
+ "Knowledge Base": "📚 Explains game concepts, terminology, and mechanics",
354
+ "Pregame Strategy": "🎯 Champion select, draft strategy, and ban recommendations"
355
+ }
356
+
357
+ for name, desc in agent_descriptions.items():
358
+ print(f"\n{desc}")
359
+ print(f" Agent: {name}")
youtube_scraper.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YouTube Web Scraper
3
+ Alternative to YouTube Data API - scrapes video data directly from YouTube pages
4
+ Useful as backup when API quota is exceeded or for additional metadata
5
+ """
6
+
7
+ import requests
8
+ from bs4 import BeautifulSoup
9
+ from typing import List, Dict, Optional
10
+ import re
11
+ import json
12
+ from urllib.parse import quote_plus, urljoin
13
+
14
+
15
+ class YouTubeScraper:
16
+ """Scrape YouTube video data without using API quota"""
17
+
18
+ BASE_URL = "https://www.youtube.com"
19
+ SEARCH_URL = f"{BASE_URL}/results"
20
+
21
+ def __init__(self):
22
+ self.session = requests.Session()
23
+ self.session.headers.update({
24
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
25
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
26
+ 'Accept-Language': 'en-US,en;q=0.9',
27
+ 'Accept-Encoding': 'gzip, deflate, br',
28
+ 'DNT': '1',
29
+ 'Connection': 'keep-alive',
30
+ 'Upgrade-Insecure-Requests': '1'
31
+ })
32
+
33
+ def search_videos(self, query: str, max_results: int = 5) -> List[Dict]:
34
+ """
35
+ Search YouTube and scrape video results
36
+
37
+ Args:
38
+ query: Search query
39
+ max_results: Maximum number of results to return
40
+
41
+ Returns:
42
+ List of video dictionaries
43
+ """
44
+ try:
45
+ # Construct search URL
46
+ encoded_query = quote_plus(query)
47
+ url = f"{self.SEARCH_URL}?search_query={encoded_query}"
48
+
49
+ print(f"🔍 Scraping YouTube search: {query}")
50
+
51
+ response = self.session.get(url, timeout=15)
52
+ response.raise_for_status()
53
+
54
+ # Parse HTML
55
+ soup = BeautifulSoup(response.content, 'html.parser')
56
+
57
+ # YouTube embeds data in JavaScript - extract it
58
+ videos = self._extract_video_data(soup, max_results)
59
+
60
+ if not videos:
61
+ print("⚠️ No videos found via scraping, trying alternative method")
62
+ videos = self._extract_from_scripts(response.text, max_results)
63
+
64
+ print(f"✅ Scraped {len(videos)} videos from YouTube")
65
+ return videos[:max_results]
66
+
67
+ except Exception as e:
68
+ print(f"❌ YouTube scraping error: {e}")
69
+ return []
70
+
71
+ def _extract_video_data(self, soup: BeautifulSoup, max_results: int) -> List[Dict]:
72
+ """Extract video data from HTML elements"""
73
+ videos = []
74
+
75
+ try:
76
+ # YouTube uses various class names - try multiple patterns
77
+ video_selectors = [
78
+ 'ytd-video-renderer',
79
+ 'ytd-playlist-video-renderer',
80
+ 'ytd-grid-video-renderer'
81
+ ]
82
+
83
+ for selector in video_selectors:
84
+ video_elements = soup.find_all(selector)
85
+
86
+ for element in video_elements[:max_results]:
87
+ if len(videos) >= max_results:
88
+ break
89
+
90
+ try:
91
+ video_data = self._parse_video_element(element)
92
+ if video_data:
93
+ videos.append(video_data)
94
+ except Exception as e:
95
+ print(f"⚠️ Error parsing video element: {e}")
96
+ continue
97
+
98
+ if videos:
99
+ break # Found videos, no need to try other selectors
100
+
101
+ return videos
102
+
103
+ except Exception as e:
104
+ print(f"⚠️ HTML extraction error: {e}")
105
+ return []
106
+
107
+ def _parse_video_element(self, element) -> Optional[Dict]:
108
+ """Parse individual video element"""
109
+ try:
110
+ # Extract video ID
111
+ video_link = element.find('a', id='video-title')
112
+ if not video_link:
113
+ return None
114
+
115
+ video_id = None
116
+ href = video_link.get('href', '')
117
+ if '/watch?v=' in href:
118
+ video_id = href.split('/watch?v=')[1].split('&')[0]
119
+
120
+ if not video_id:
121
+ return None
122
+
123
+ # Extract title
124
+ title = video_link.get('title', '') or video_link.get_text(strip=True)
125
+
126
+ # Extract channel name
127
+ channel_elem = element.find('ytd-channel-name') or element.find('a', class_='yt-simple-endpoint')
128
+ channel = channel_elem.get_text(strip=True) if channel_elem else 'Unknown'
129
+
130
+ # Extract view count and duration
131
+ metadata = element.find_all('span', class_='style-scope ytd-video-meta-block')
132
+ views_text = 'N/A'
133
+ duration = 'N/A'
134
+
135
+ for meta in metadata:
136
+ text = meta.get_text(strip=True)
137
+ if 'view' in text.lower():
138
+ views_text = text
139
+ elif ':' in text:
140
+ duration = text
141
+
142
+ # Extract thumbnail
143
+ thumbnail_elem = element.find('img')
144
+ thumbnail = thumbnail_elem.get('src', '') if thumbnail_elem else ''
145
+
146
+ # Parse view count
147
+ views = self._parse_view_count(views_text)
148
+
149
+ return {
150
+ 'video_id': video_id,
151
+ 'title': title,
152
+ 'url': f"https://www.youtube.com/watch?v={video_id}",
153
+ 'channel': channel,
154
+ 'views': views,
155
+ 'views_text': views_text,
156
+ 'duration': duration,
157
+ 'thumbnail': thumbnail,
158
+ 'description': title[:200] + '...', # Use title as description
159
+ }
160
+
161
+ except Exception as e:
162
+ print(f"⚠️ Parse element error: {e}")
163
+ return None
164
+
165
+ def _extract_from_scripts(self, html_text: str, max_results: int) -> List[Dict]:
166
+ """Extract video data from embedded JavaScript"""
167
+ videos = []
168
+
169
+ try:
170
+ # YouTube embeds data in ytInitialData variable
171
+ pattern = r'var ytInitialData = ({.*?});'
172
+ match = re.search(pattern, html_text, re.DOTALL)
173
+
174
+ if match:
175
+ try:
176
+ data = json.loads(match.group(1))
177
+
178
+ # Navigate YouTube's data structure
179
+ contents = (data.get('contents', {})
180
+ .get('twoColumnSearchResultsRenderer', {})
181
+ .get('primaryContents', {})
182
+ .get('sectionListRenderer', {})
183
+ .get('contents', []))
184
+
185
+ for section in contents:
186
+ item_section = section.get('itemSectionRenderer', {})
187
+ video_items = item_section.get('contents', [])
188
+
189
+ for item in video_items[:max_results]:
190
+ if len(videos) >= max_results:
191
+ break
192
+
193
+ video_renderer = item.get('videoRenderer')
194
+ if video_renderer:
195
+ video_data = self._parse_video_renderer(video_renderer)
196
+ if video_data:
197
+ videos.append(video_data)
198
+
199
+ except json.JSONDecodeError as e:
200
+ print(f"⚠️ JSON decode error: {e}")
201
+
202
+ return videos
203
+
204
+ except Exception as e:
205
+ print(f"⚠️ Script extraction error: {e}")
206
+ return []
207
+
208
+ def _parse_video_renderer(self, renderer: Dict) -> Optional[Dict]:
209
+ """Parse video data from ytInitialData structure"""
210
+ try:
211
+ video_id = renderer.get('videoId')
212
+ if not video_id:
213
+ return None
214
+
215
+ # Extract title
216
+ title_runs = renderer.get('title', {}).get('runs', [])
217
+ title = title_runs[0].get('text', 'Unknown') if title_runs else 'Unknown'
218
+
219
+ # Extract channel
220
+ owner_text = renderer.get('ownerText', {}).get('runs', [])
221
+ channel = owner_text[0].get('text', 'Unknown') if owner_text else 'Unknown'
222
+
223
+ # Extract view count
224
+ view_count_text = renderer.get('viewCountText', {}).get('simpleText', '0 views')
225
+ views = self._parse_view_count(view_count_text)
226
+
227
+ # Extract duration
228
+ length_text = renderer.get('lengthText', {}).get('simpleText', 'N/A')
229
+
230
+ # Extract thumbnail
231
+ thumbnails = renderer.get('thumbnail', {}).get('thumbnails', [])
232
+ thumbnail = thumbnails[-1].get('url', '') if thumbnails else ''
233
+
234
+ return {
235
+ 'video_id': video_id,
236
+ 'title': title,
237
+ 'url': f"https://www.youtube.com/watch?v={video_id}",
238
+ 'channel': channel,
239
+ 'views': views,
240
+ 'views_text': view_count_text,
241
+ 'duration': length_text,
242
+ 'thumbnail': thumbnail,
243
+ 'description': title[:200] + '...',
244
+ }
245
+
246
+ except Exception as e:
247
+ print(f"⚠️ Renderer parse error: {e}")
248
+ return None
249
+
250
+ def _parse_view_count(self, view_text: str) -> int:
251
+ """Parse view count from text like '1.2M views' or '50K views'"""
252
+ try:
253
+ # Remove 'views' and extra spaces
254
+ view_text = view_text.lower().replace('views', '').replace('view', '').strip()
255
+
256
+ # Handle K, M, B suffixes
257
+ multipliers = {
258
+ 'k': 1_000,
259
+ 'm': 1_000_000,
260
+ 'b': 1_000_000_000
261
+ }
262
+
263
+ for suffix, multiplier in multipliers.items():
264
+ if suffix in view_text:
265
+ number = float(view_text.replace(suffix, '').strip())
266
+ return int(number * multiplier)
267
+
268
+ # Try to parse as plain number
269
+ view_text = re.sub(r'[^\d.]', '', view_text)
270
+ return int(float(view_text)) if view_text else 0
271
+
272
+ except Exception:
273
+ return 0
274
+
275
+ def format_video_list(self, videos: List[Dict]) -> str:
276
+ """Format video list for display"""
277
+ if not videos:
278
+ return "No videos found."
279
+
280
+ result = ""
281
+ for i, video in enumerate(videos, 1):
282
+ views_k = video['views'] / 1000 if video['views'] > 0 else 0
283
+ result += f"{i}. **{video['title']}**\n"
284
+ result += f" • Channel: {video['channel']}\n"
285
+ result += f" • Duration: {video['duration']} | Views: {views_k:.1f}K\n"
286
+ result += f" • Link: {video['url']}\n\n"
287
+
288
+ return result
289
+
290
+
291
+ # Test the scraper
292
+ if __name__ == "__main__":
293
+ print("🎬 Testing YouTube Scraper\n")
294
+
295
+ scraper = YouTubeScraper()
296
+
297
+ # Test searches
298
+ test_queries = [
299
+ "League of Legends Ahri guide 2024",
300
+ "Ahri vs Zed gameplay",
301
+ "League wave management tutorial"
302
+ ]
303
+
304
+ for query in test_queries:
305
+ print(f"\n{'='*60}")
306
+ print(f"Testing: {query}")
307
+ print('='*60)
308
+
309
+ videos = scraper.search_videos(query, max_results=3)
310
+
311
+ if videos:
312
+ print(f"✅ Found {len(videos)} videos:\n")
313
+ print(scraper.format_video_list(videos))
314
+ else:
315
+ print("❌ No videos found")
316
+
317
+ print()