Spaces:
No application file
No application file
Commit
·
e30aaa7
1
Parent(s):
befb7f7
[UI COMPONENTS] :: Player card, Game card, and Team card added
Browse files- api/event_handlers/gradio_handler.py +184 -9
- api/tools/__init__.py +2 -0
- api/tools/team_search.py +92 -1
- api/workflows/base.py +2 -0
api/event_handlers/gradio_handler.py
CHANGED
@@ -5,12 +5,61 @@ from langchain_core.outputs.llm_result import LLMResult
|
|
5 |
from typing import List
|
6 |
from langchain_core.messages import BaseMessage
|
7 |
|
|
|
|
|
8 |
image_base = """
|
9 |
-
<
|
10 |
-
|
11 |
-
style="
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
"""
|
|
|
14 |
team_image_map = {
|
15 |
'everglade-fc': 'Everglade_FC',
|
16 |
'fraser-valley-united': 'Fraser_Valley_United',
|
@@ -68,12 +117,129 @@ class GradioEventHandler(AsyncCallbackHandler):
|
|
68 |
print(f"\n{Fore.CYAN}[TOOL END] {output}{Style.RESET_ALL}")
|
69 |
for doc in output:
|
70 |
if doc.metadata.get("show_profile_card"):
|
71 |
-
|
72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
self.ots_box(img)
|
74 |
-
|
75 |
-
|
76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
|
78 |
async def on_tool_start(self, input: any, *args, **kwargs):
|
79 |
self.info_box(input.get("name", "[TOOL START]"))
|
@@ -95,3 +261,12 @@ class GradioEventHandler(AsyncCallbackHandler):
|
|
95 |
@staticmethod
|
96 |
def get_image_filename(doc):
|
97 |
return f'{team_image_map.get(doc.metadata.get("team"))}_{doc.metadata.get("number")}.png'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
from typing import List
|
6 |
from langchain_core.messages import BaseMessage
|
7 |
|
8 |
+
TEAM_LOGO_BASE_URL = "https://huggingface.co/spaces/yamilsteven/ifx-assets/resolve/main/assets/team_logos/"
|
9 |
+
|
10 |
image_base = """
|
11 |
+
<div style="background-color: #2C2C2C; border-radius: 10px; padding: 25px; display: flex; align-items: center; width: 450px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);">
|
12 |
+
<div style="margin-right: 25px;">
|
13 |
+
<img src="https://huggingface.co/spaces/ryanbalch/IFX-huge-league/resolve/main/assets/profiles/players_pics/{filename}" alt="{filename} Pic" style="width: 100px; height: 100px; border-radius: 50%; object-fit: cover; border: 3px solid #444;">
|
14 |
+
</div>
|
15 |
+
<div style="color: #E0E0E0;">
|
16 |
+
<h2 style="margin-top: 0; margin-bottom: 8px; font-size: 22px; color: #FFFFFF;">{player_name} - #{player_number} (DL)</h2>
|
17 |
+
<p style="margin: 6px 0; font-size: 15px; color: #B0B0B0;">Ht: {height} | Wt: {weight} lbs</p>
|
18 |
+
<p style="margin: 6px 0; font-size: 15px; color: #B0B0B0;">Team: {team_name}</p>
|
19 |
+
<p style="margin: 6px 0; font-size: 15px; color: #B0B0B0;">Experience: 6 Years</p>
|
20 |
+
<a href="https://www.instagram.com/{instagram_url}" style="background-color: #C00000; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; margin-top: 15px; font-size: 14px; text-align: center;">Instagram Profile</a>
|
21 |
+
</div>
|
22 |
+
</div>
|
23 |
+
"""
|
24 |
+
|
25 |
+
game_card_html = """
|
26 |
+
<div style="text-align: center; padding: 20px; margin: 10px 0; border-radius: 8px; background-color: #3A3B3C; color: #FFFFFF; font-family: Arial, sans-serif; max-width: 500px; margin-left: auto; margin-right: auto;">
|
27 |
+
<h3 style="margin-top: 0; margin-bottom: 10px; font-size: 20px; color: #E0E0E0;">{game_title}</h3>
|
28 |
+
<div style="display: flex; justify-content: space-around; align-items: center; margin-bottom: 15px;">
|
29 |
+
<div style="text-align: center; width: 40%;">
|
30 |
+
<img src="{team1_logo_url}"
|
31 |
+
alt="{team_home} Logo"
|
32 |
+
style="max-width: 70px; max-height: 70px; margin-bottom: 5px; vertical-align: middle; display: block; margin-left: auto; margin-right: auto;"
|
33 |
+
onerror="this.onerror=null; this.src='{default_logo_url}';">
|
34 |
+
<p style="font-size: 16px; font-weight: bold; margin: 5px 0;">{team_home}</p>
|
35 |
+
<p style="font-size: 24px; font-weight: bold; margin: 0;">{team1_score}</p>
|
36 |
+
</div>
|
37 |
+
<p style="font-size: 20px; margin: 0 10px;">vs</p>
|
38 |
+
<div style="text-align: center; width: 40%;">
|
39 |
+
<img src="{team2_logo_url}"
|
40 |
+
alt="{team_away} Logo"
|
41 |
+
style="max-width: 70px; max-height: 70px; margin-bottom: 5px; vertical-align: middle; display: block; margin-left: auto; margin-right: auto;"
|
42 |
+
onerror="this.onerror=null; this.src='{default_logo_url}';">
|
43 |
+
<p style="font-size: 16px; font-weight: bold; margin: 5px 0;">{team_away}</p>
|
44 |
+
<p style="font-size: 24px; font-weight: bold; margin: 0;">{team2_score}</p>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
<div style="margin-top: 20px; border-top: 1px solid #555; padding-top: 15px;">
|
48 |
+
<h4 style="margin-top: 0; margin-bottom: 10px; font-size: 16px; color: #D0D0D0; text-align: left;">Goal Highlights:</h4>
|
49 |
+
{highlights}
|
50 |
+
</div>
|
51 |
+
</div>
|
52 |
+
"""
|
53 |
+
|
54 |
+
team_info_card_html = """
|
55 |
+
<div style="text-align: center; padding: 50px; margin:10px 0; border-radius: 8px; background-color: #FFFFFF; color: #333333; font-family: Arial, sans-serif; max-width: 450px; margin-left: auto; margin-right: auto; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
56 |
+
<img src="{team_logo_url}" alt="{team_display_name} Logo" style="max-width: 80px; max-height: 80px; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto; border-radius: 5px;" onerror="this.onerror=null; this.src='{default_logo_url}';">
|
57 |
+
<h3 style="margin-top: 0; margin-bottom: 8px; font-size: 22px; color: #000000;">{team_display_name}</h3>
|
58 |
+
<p style="font-size: 15px; color: #333333; margin-bottom: 15px;">{city_display}</p>
|
59 |
+
{team_page_cta_html}
|
60 |
+
</div>
|
61 |
"""
|
62 |
+
|
63 |
team_image_map = {
|
64 |
'everglade-fc': 'Everglade_FC',
|
65 |
'fraser-valley-united': 'Fraser_Valley_United',
|
|
|
117 |
print(f"\n{Fore.CYAN}[TOOL END] {output}{Style.RESET_ALL}")
|
118 |
for doc in output:
|
119 |
if doc.metadata.get("show_profile_card"):
|
120 |
+
raw_player_name = doc.metadata.get("name", "unknown-player")
|
121 |
+
player_name = "Unknown Player"
|
122 |
+
if raw_player_name != "unknown-player":
|
123 |
+
parts = raw_player_name.split('-')
|
124 |
+
if len(parts) == 2:
|
125 |
+
first_name = parts[0].capitalize()
|
126 |
+
last_name = parts[1].capitalize()
|
127 |
+
player_name = f"{last_name} {first_name}"
|
128 |
+
else:
|
129 |
+
player_name = raw_player_name.replace('-', ' ').title()
|
130 |
+
else:
|
131 |
+
player_name = "Unknown Player"
|
132 |
+
|
133 |
+
team_slug = doc.metadata.get("team", "unknown-team")
|
134 |
+
formatted_team_name = team_slug.replace('-', ' ').title()
|
135 |
+
if team_slug == "unknown-team":
|
136 |
+
formatted_team_name = "Unknown Team"
|
137 |
+
|
138 |
+
player_number = doc.metadata.get("number", "N/A")
|
139 |
+
height = doc.metadata.get("height", "175")
|
140 |
+
weight = doc.metadata.get("weight", "150")
|
141 |
+
instagram_url = doc.metadata.get("instagram_url", raw_player_name)
|
142 |
+
|
143 |
+
img = image_base.format(
|
144 |
+
filename=self.get_image_filename(doc),
|
145 |
+
player_name=player_name,
|
146 |
+
team_name=formatted_team_name,
|
147 |
+
player_number=player_number,
|
148 |
+
height=height,
|
149 |
+
weight=weight,
|
150 |
+
instagram_url=instagram_url
|
151 |
+
)
|
152 |
self.ots_box(img)
|
153 |
+
return
|
154 |
+
|
155 |
+
if output and isinstance(output[0], object) and hasattr(output[0], 'metadata') and output[0].metadata.get('type') == 'event':
|
156 |
+
game_teams_set = set()
|
157 |
+
game_events_details = []
|
158 |
+
raw_game_name = "Unknown Game"
|
159 |
+
|
160 |
+
for i, doc in enumerate(output):
|
161 |
+
if hasattr(doc, 'metadata') and doc.metadata.get('team'):
|
162 |
+
game_teams_set.add(doc.metadata.get('team'))
|
163 |
+
if i == 0:
|
164 |
+
raw_game_name = doc.metadata.get('game_name', raw_game_name)
|
165 |
+
|
166 |
+
if hasattr(doc, 'metadata') and doc.metadata.get('event') == 'Goal':
|
167 |
+
event_desc = doc.metadata.get('description', 'Goal scored')
|
168 |
+
event_minute = doc.metadata.get('minute', '')
|
169 |
+
event_team = doc.metadata.get('team', '')
|
170 |
+
game_events_details.append(f"({event_minute}') {event_team}: {event_desc}")
|
171 |
+
|
172 |
+
if len(game_teams_set) >= 1:
|
173 |
+
team_list = list(game_teams_set)
|
174 |
+
team1_name_str = team_list[0]
|
175 |
+
team2_name_str = team_list[1] if len(team_list) > 1 else "(opponent not specified)"
|
176 |
+
|
177 |
+
score_team1 = 0
|
178 |
+
score_team2 = 0
|
179 |
+
for doc in output:
|
180 |
+
if hasattr(doc, 'metadata') and doc.metadata.get('event') == 'Goal':
|
181 |
+
scoring_team = doc.metadata.get('team')
|
182 |
+
if scoring_team == team1_name_str:
|
183 |
+
score_team1 += 1
|
184 |
+
elif scoring_team == team2_name_str:
|
185 |
+
score_team2 += 1
|
186 |
+
|
187 |
+
game_title_str = raw_game_name.replace('_', ' ').title()
|
188 |
+
highlights_html_str = "<ul style='list-style-type: none; padding-left: 0; text-align: left;'>"
|
189 |
+
for highlight in game_events_details[:3]:
|
190 |
+
highlights_html_str += f"<li style='margin-bottom: 5px; font-size: 14px;'>{highlight}</li>"
|
191 |
+
highlights_html_str += "</ul>"
|
192 |
+
if not game_events_details:
|
193 |
+
highlights_html_str = "<p style='font-size: 14px;'>No goal highlights available.</p>"
|
194 |
+
|
195 |
+
log_msg = f"Game: {game_title_str} | {team1_name_str} {score_team1} - {score_team2} {team2_name_str}"
|
196 |
+
team1_logo_filename = GradioEventHandler.get_team_logo_slug(team1_name_str)
|
197 |
+
team2_logo_filename = GradioEventHandler.get_team_logo_slug(team2_name_str)
|
198 |
+
|
199 |
+
team1_logo_url = f"{TEAM_LOGO_BASE_URL}{team1_logo_filename}"
|
200 |
+
team2_logo_url = f"{TEAM_LOGO_BASE_URL}{team2_logo_filename}"
|
201 |
+
default_logo_url = f"{TEAM_LOGO_BASE_URL}default.png"
|
202 |
+
|
203 |
+
formatted_game_html = game_card_html.format(
|
204 |
+
game_title=game_title_str,
|
205 |
+
team_home=team1_name_str,
|
206 |
+
team_away=team2_name_str,
|
207 |
+
team1_score=score_team1,
|
208 |
+
team2_score=score_team2,
|
209 |
+
highlights=highlights_html_str,
|
210 |
+
team1_logo_url=team1_logo_url,
|
211 |
+
team2_logo_url=team2_logo_url,
|
212 |
+
default_logo_url=default_logo_url
|
213 |
+
)
|
214 |
+
self.ots_box(formatted_game_html)
|
215 |
+
return
|
216 |
+
else:
|
217 |
+
print(f"\n{Fore.RED}[TOOL END - GAME CARD] Not enough team data found in events.{Style.RESET_ALL}")
|
218 |
+
|
219 |
+
elif isinstance(output, list) and output:
|
220 |
+
doc_for_team_card = output[0]
|
221 |
+
if hasattr(doc_for_team_card, 'metadata') and doc_for_team_card.metadata.get("show_team_card"):
|
222 |
+
team_name_from_meta = doc_for_team_card.metadata.get("team_name", "Unknown Team")
|
223 |
+
city_raw = doc_for_team_card.metadata.get("city", "N/A")
|
224 |
+
display_team_name = str(team_name_from_meta).replace('-', ' ').replace('_', ' ').title()
|
225 |
+
city_display = city_raw.title() if city_raw != "N/A" else "Location N/A"
|
226 |
+
logo_filename = GradioEventHandler.get_team_logo_slug(team_name_from_meta)
|
227 |
+
team_specific_logo_url = f"{TEAM_LOGO_BASE_URL}{logo_filename}"
|
228 |
+
current_default_logo_url = f"{TEAM_LOGO_BASE_URL}default.png"
|
229 |
+
team_id_slug = doc_for_team_card.metadata.get("team_id", "")
|
230 |
+
team_page_url = f"https://www.team.com/{team_id_slug}" if team_id_slug else "#"
|
231 |
+
team_page_cta_html = f'''<a href="{team_page_url}" target="_blank" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; font-size: 14px; margin-top: 10px;">Visit Team Page</a>'''
|
232 |
+
|
233 |
+
formatted_team_card_html = team_info_card_html.format(
|
234 |
+
team_logo_url=team_specific_logo_url,
|
235 |
+
team_display_name=display_team_name,
|
236 |
+
city_display=city_display,
|
237 |
+
default_logo_url=current_default_logo_url,
|
238 |
+
team_page_cta_html=team_page_cta_html
|
239 |
+
)
|
240 |
+
|
241 |
+
self.ots_box(formatted_team_card_html)
|
242 |
+
return
|
243 |
|
244 |
async def on_tool_start(self, input: any, *args, **kwargs):
|
245 |
self.info_box(input.get("name", "[TOOL START]"))
|
|
|
261 |
@staticmethod
|
262 |
def get_image_filename(doc):
|
263 |
return f'{team_image_map.get(doc.metadata.get("team"))}_{doc.metadata.get("number")}.png'
|
264 |
+
|
265 |
+
@staticmethod
|
266 |
+
def get_team_logo_slug(team_name: str) -> str:
|
267 |
+
if not team_name or team_name == "(opponent not specified)":
|
268 |
+
return "default.png"
|
269 |
+
# Normalize accented for the Yucatán logo
|
270 |
+
normalized_team_name = team_name.lower().replace('á', 'a')
|
271 |
+
slug = normalized_team_name.replace(' ', '-')
|
272 |
+
return slug + ".png"
|
api/tools/__init__.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1 |
from langchain_core.documents import Document
|
2 |
from .player_search import PlayerSearchTool
|
3 |
from .game_search import GameSearchTool
|
|
|
4 |
|
5 |
__all__ = [
|
6 |
"PlayerSearchTool",
|
7 |
"GameSearchTool",
|
|
|
8 |
"Document",
|
9 |
]
|
|
|
1 |
from langchain_core.documents import Document
|
2 |
from .player_search import PlayerSearchTool
|
3 |
from .game_search import GameSearchTool
|
4 |
+
from .team_search import TeamSearchTool
|
5 |
|
6 |
__all__ = [
|
7 |
"PlayerSearchTool",
|
8 |
"GameSearchTool",
|
9 |
+
"TeamSearchTool",
|
10 |
"Document",
|
11 |
]
|
api/tools/team_search.py
CHANGED
@@ -1 +1,92 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Team Search Tool
|
3 |
+
|
4 |
+
This module defines the TeamSearchTool, a LangChain-compatible tool for searching soccer teams
|
5 |
+
in the fictional Huge League using the project's vector store.
|
6 |
+
"""
|
7 |
+
|
8 |
+
from pydantic import BaseModel, Field
|
9 |
+
from langchain.tools import BaseTool
|
10 |
+
from langchain_core.documents import Document
|
11 |
+
from typing import Type, List, Optional
|
12 |
+
from langchain.callbacks.manager import (
|
13 |
+
AsyncCallbackManagerForToolRun,
|
14 |
+
CallbackManagerForToolRun,
|
15 |
+
)
|
16 |
+
from data.vectorstore_singleton import get_vector_store
|
17 |
+
|
18 |
+
vector_store = get_vector_store()
|
19 |
+
|
20 |
+
|
21 |
+
class TeamSearchInputSchema(BaseModel):
|
22 |
+
team_query: str = Field(description=(
|
23 |
+
"The search query to identify a soccer team in the fictional league. "
|
24 |
+
))
|
25 |
+
|
26 |
+
class TeamSearchTool(BaseTool):
|
27 |
+
name: str = "team_search"
|
28 |
+
description: str = (
|
29 |
+
"Searches for a specific soccer team in the fictional league based on its name. "
|
30 |
+
"Returns information about the team, which can be used to display a team card."
|
31 |
+
)
|
32 |
+
args_schema: Type[BaseModel] = TeamSearchInputSchema
|
33 |
+
|
34 |
+
def _run(
|
35 |
+
self,
|
36 |
+
team_query: str,
|
37 |
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
38 |
+
) -> List[Document]:
|
39 |
+
"""Search for a team using the vector store."""
|
40 |
+
results = vector_store.similarity_search(
|
41 |
+
query=team_query,
|
42 |
+
k=1,
|
43 |
+
filter=lambda doc: doc.metadata.get("type") == "team",
|
44 |
+
)
|
45 |
+
|
46 |
+
processed_results = []
|
47 |
+
for doc in results:
|
48 |
+
team_name_found = doc.metadata.get("name", team_query)
|
49 |
+
|
50 |
+
doc.metadata["show_team_card"] = True
|
51 |
+
doc.metadata["team_name"] = team_name_found
|
52 |
+
doc.metadata.pop("country", None)
|
53 |
+
doc.metadata.pop("description", None)
|
54 |
+
if "city" not in doc.metadata:
|
55 |
+
doc.metadata["city"] = "N/A"
|
56 |
+
|
57 |
+
processed_results.append(doc)
|
58 |
+
|
59 |
+
return processed_results
|
60 |
+
|
61 |
+
async def _arun(
|
62 |
+
self,
|
63 |
+
team_query: str,
|
64 |
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
65 |
+
) -> List[Document]:
|
66 |
+
"""Asynchronously searches for a team using the vector store."""
|
67 |
+
found_docs = await vector_store.asimilarity_search(
|
68 |
+
query=team_query,
|
69 |
+
k=3,
|
70 |
+
metadata={"type": "team"} # Use metadata filter instead of filter function
|
71 |
+
)
|
72 |
+
|
73 |
+
processed_results = []
|
74 |
+
if found_docs:
|
75 |
+
doc = found_docs[0]
|
76 |
+
if doc.metadata.get("type") == "team" and doc.metadata.get("name"):
|
77 |
+
metadata = {
|
78 |
+
"show_team_card": True,
|
79 |
+
"team_name": doc.metadata.get("name", "Unknown Team"),
|
80 |
+
"team_id": doc.metadata.get("id", doc.metadata.get("name", "unknown-id").lower().replace(" ", "-")),
|
81 |
+
"city": doc.metadata.get("city", "N/A"),
|
82 |
+
}
|
83 |
+
page_content = f"Found: {metadata['team_name']}. Location: {metadata.get('city')}."
|
84 |
+
processed_doc = Document(page_content=page_content, metadata=metadata)
|
85 |
+
processed_results.append(processed_doc)
|
86 |
+
else:
|
87 |
+
print(f"Found document for query '{team_query}' but it's not a valid team entry or lacks name.")
|
88 |
+
|
89 |
+
if not processed_results:
|
90 |
+
print(f"No team found for query: {team_query} after vector search and filtering.")
|
91 |
+
|
92 |
+
return processed_results
|
api/workflows/base.py
CHANGED
@@ -28,12 +28,14 @@ from tools import (
|
|
28 |
Document,
|
29 |
PlayerSearchTool,
|
30 |
GameSearchTool,
|
|
|
31 |
)
|
32 |
|
33 |
|
34 |
available_tools = [
|
35 |
GameSearchTool(),
|
36 |
PlayerSearchTool(),
|
|
|
37 |
]
|
38 |
tool_node = ToolNode(available_tools)
|
39 |
|
|
|
28 |
Document,
|
29 |
PlayerSearchTool,
|
30 |
GameSearchTool,
|
31 |
+
TeamSearchTool,
|
32 |
)
|
33 |
|
34 |
|
35 |
available_tools = [
|
36 |
GameSearchTool(),
|
37 |
PlayerSearchTool(),
|
38 |
+
TeamSearchTool(),
|
39 |
]
|
40 |
tool_node = ToolNode(available_tools)
|
41 |
|