zypchn commited on
Commit
896c7d9
·
verified ·
1 Parent(s): 6cce8c7

Update src/tools.py

Browse files
Files changed (1) hide show
  1. src/tools.py +99 -53
src/tools.py CHANGED
@@ -1,24 +1,7 @@
1
  import pandas as pd
2
  from rapidfuzz import process, fuzz
3
-
4
- # Data Loading ---------------------------------------------------------------
5
- try:
6
- knowledge_base = pd.read_csv("mcp_knowledge_base.csv")
7
- knowledge_base_latest = pd.read_csv("mcp_knowledge_base_latest.csv")
8
- ALL_PROD_NAMES = knowledge_base.prod_name.values
9
- ALL_ARTIST_NAMES = knowledge_base.artist.values
10
- ALL_SET_NAMES = knowledge_base.set_name.values
11
-
12
- except Exception as e:
13
- # Handle data loading error
14
- print(f"ERROR loading data for tools: {e}")
15
- knowledge_base = pd.DataFrame()
16
- knowledge_base_latest = pd.DataFrame()
17
- ALL_PROD_NAMES = []
18
- ALL_ARTIST_NAMES = []
19
- ALL_SET_NAMES = []
20
- # ---------------------------------------------------------------------------
21
-
22
 
23
  class PokemonAdvisorTools():
24
  """
@@ -26,21 +9,66 @@ class PokemonAdvisorTools():
26
  for the cAsh MCP Robo-Advisor.
27
  """
28
 
29
- knowledge_base = knowledge_base
30
- knowledge_base_latest = knowledge_base_latest
31
- ALL_PROD_NAMES = ALL_PROD_NAMES
32
- ALL_ARTIST_NAMES = ALL_ARTIST_NAMES
33
- ALL_SET_NAMES = ALL_SET_NAMES
34
-
35
- print(len(knowledge_base))
36
- print(len(knowledge_base_latest))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  def get_data_shape(self) -> int:
39
  """
40
  Retrieves the number of set names.
41
  Use this when user asks about the number of unique sets.
42
  Returns:
43
- int: lenght of knowledge base
44
  """
45
  return len(self.ALL_SET_NAMES)
46
 
@@ -54,7 +82,8 @@ class PokemonAdvisorTools():
54
  Returns:
55
  list: A list of 'prod_name's that matches the 'name_query'
56
  """
57
- if not self.ALL_PROD_NAMES.any(): return {"error": "Data not loaded."}
 
58
 
59
  prod_names_match = process.extract(name_query, self.ALL_PROD_NAMES, scorer=fuzz.WRatio, limit=5)
60
  return [name[0].replace("_", " ") for name in prod_names_match]
@@ -74,7 +103,8 @@ class PokemonAdvisorTools():
74
  dict: A dictionary containing 'used_price', 'graded_price', 'trend_6',
75
  and other key metrics. Returns an 'error' key if not found.
76
  """
77
- if not self.ALL_PROD_NAMES.any(): return {"error": "Data not loaded."}
 
78
 
79
  match = process.extractOne(name_query, self.ALL_PROD_NAMES, scorer=fuzz.WRatio)
80
  if not match or match[1] < 70:
@@ -84,11 +114,10 @@ class PokemonAdvisorTools():
84
  card_df = self.knowledge_base_latest[self.knowledge_base_latest["prod_name"] == prod_name]
85
 
86
  if card_df.empty:
87
- return {"error": f"Data missing for '{prod_name}'."}
88
 
89
  return card_df.to_dict(orient="records")[0]
90
 
91
-
92
  def find_grading_opportunities(self, max_price: float = 100, min_profit: float = 20) -> list:
93
  """
94
  Scans the market for 'Arbitrage' opportunities where the gap between the Raw
@@ -110,7 +139,7 @@ class PokemonAdvisorTools():
110
  min_profit_grades = profitable_grades[profitable_grades["grade_profit"] >= min_profit]
111
  min_profit_grades = min_profit_grades.sort_values(
112
  by="grade_profit", ascending=False
113
- ).head(10)
114
  output_columns = [
115
  "prod_name",
116
  "used_price",
@@ -123,7 +152,7 @@ class PokemonAdvisorTools():
123
  min_profit_grades = min_profit_grades[output_columns]
124
  return min_profit_grades.to_dict(orient="records")
125
 
126
- def get_market_movers(self, sort_by: str ="uptrend", interval: int = 6, market_type: str ="used") -> list:
127
  """
128
  Identifies cards with the strongest positive or negative price trends over a sustained period (3 or 6 months).
129
 
@@ -140,8 +169,11 @@ class PokemonAdvisorTools():
140
  Returns:
141
  list: A list of the top 10 cards matching the trend criteria, including their percentage change.
142
  """
143
- market_move_data = self.knowledge_base_latest.sort(by=f"{market_type}_trend_{interval}", ascending=(not sort_by=="uptrend")).head(10)
144
- output_columns = ["prod_name", "used_price", "graded_price"]
 
 
 
145
  market_move_data = market_move_data[output_columns]
146
  return market_move_data.to_dict(orient="records")
147
 
@@ -173,8 +205,8 @@ class PokemonAdvisorTools():
173
  return {"error": "Invalid 'interval' value. Must be 3 or 6."}
174
 
175
  card_info = self.get_card_info(card_name)
176
- if not card_info:
177
- return {"error": f"Card not found for query: {card_name}"}
178
 
179
  if interval not in [3, 6]:
180
  return {"error": f"Invalid interval requested: {interval}. Only 3 or 6 months are supported."}
@@ -198,13 +230,13 @@ class PokemonAdvisorTools():
198
  graded_volatility = card_info.get("graded_vol_6")
199
 
200
  if used_volatility is None or graded_volatility is None:
201
- return {"error": f"Volatility data missing for {card_name} at {interval} months. Check if card exists in the full knowledge base."}
202
 
203
  return {
204
- f"used_volatility": used_volatility,
205
- f"graded_volatility": graded_volatility,
206
- f"used_volatility_assesment_{interval}_months": self._calculate_risk_label(used_volatility, used_vol_low_threshold, used_vol_high_threshold),
207
- f"graded_volatility_assesment_{interval}_months": self._calculate_risk_label(graded_volatility, graded_vol_low_threshold, graded_vol_high_threshold),
208
  }
209
 
210
  def get_roi_metrics(self, card_name: str) -> dict:
@@ -220,8 +252,9 @@ class PokemonAdvisorTools():
220
  dict: Returns 3-month and 6-month ROI percentages for both Used and Graded conditions.
221
  """
222
  card_info = self.get_card_info(card_name)
223
- if not card_info:
224
- return {"error": f"Card not found for query: {card_name}. Cannot calculate ROI."}
 
225
  return {
226
  "used_price": card_info.get("used_price"),
227
  "used_return_3_months": card_info.get("used_return_3"),
@@ -255,7 +288,6 @@ class PokemonAdvisorTools():
255
  output_columns = ["prod_name", "set_name", "graded_price"]
256
  return jump_data[output_columns].to_dict(orient="records")
257
 
258
- # --- Error Handling ---
259
  else:
260
  return {"error": f"Invalid market_type '{market_type}'. Please use 'used' or 'graded'."}
261
 
@@ -266,11 +298,14 @@ class PokemonAdvisorTools():
266
  Use this for "Niche" requests or when users ask about art styles.
267
 
268
  Args:
269
- artist_name (str): The artist's name limited to ['Akira Egawa', 'Shinji Kanda', 'HYOGONOSUKE', 'sowsow', 'Tomokazu Komiya'].
270
 
271
  Returns:
272
  dict: A list of cards by that artist, sorted by profitability.
273
  """
 
 
 
274
  artist_match = process.extractOne(artist_name, self.ALL_ARTIST_NAMES, scorer=fuzz.WRatio)
275
 
276
  if not artist_match or artist_match[1] < 75:
@@ -289,7 +324,6 @@ class PokemonAdvisorTools():
289
  "grade_profit_ratio"
290
  ]
291
 
292
- # --- Error Handling ---
293
  if profitable_cards.empty:
294
  return {"result": f"No currently profitable cards found by artist {artist_name_match} in the latest data."}
295
 
@@ -306,23 +340,35 @@ class PokemonAdvisorTools():
306
  rather than specific cards.
307
 
308
  Args:
309
- set_query (str): The name of the set (e.g., "Sun & Moon"). Fuzzy matched.
310
 
311
  Returns:
312
  dict: Average trends, average profitability, and the set's 'Chase Card'.
313
- """
314
- set_name_match = process.extractOne(set_name.lower(), self.ALL_SET_NAMES, scorer=fuzz.WRatio)[0]
 
 
 
 
 
 
 
 
315
  set_card_data = self.knowledge_base_latest[self.knowledge_base_latest["set_name"] == set_name_match]
 
 
 
 
316
  total_cards = len(set_card_data)
317
  avg_trend_6 = set_card_data["used_trend_6"].mean()
318
  avg_grade_profit = set_card_data["grade_profit"].mean()
319
  chase_card_row = set_card_data.sort_values('used_price', ascending=False).iloc[0]
320
 
321
  return {
322
- "set_name": set_name_match.replace("-", " "),
323
  "total_cards_tracked": total_cards,
324
  "market_sentiment_6mo": f"{avg_trend_6:.2f}%",
325
  "avg_grading_profit": f"${avg_grade_profit:.2f}",
326
  "chase_card": chase_card_row['prod_name'],
327
- "chase_card_price": chase_card_row['used_price']
328
  }
 
1
  import pandas as pd
2
  from rapidfuzz import process, fuzz
3
+ import os
4
+ from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  class PokemonAdvisorTools():
7
  """
 
9
  for the cAsh MCP Robo-Advisor.
10
  """
11
 
12
+ def __init__(self, data_dir: str = None):
13
+ """
14
+ Initialize the tools with data loading.
15
+
16
+ Args:
17
+ data_dir: Directory containing the CSV files. If None, uses current directory.
18
+ """
19
+ # Determine data directory
20
+ if data_dir is None:
21
+ data_dir = Path(__file__).parent # Same directory as tools.py
22
+ else:
23
+ data_dir = Path(data_dir)
24
+
25
+ # Construct file paths
26
+ kb_path = data_dir / "mcp_knowledge_base.csv"
27
+ kb_latest_path = data_dir / "mcp_knowledge_base_latest.csv"
28
+
29
+ # Load data with error handling
30
+ try:
31
+ print(f"Loading data from: {data_dir}")
32
+ print(f"Looking for: {kb_path}")
33
+ print(f"Looking for: {kb_latest_path}")
34
+
35
+ if not kb_path.exists():
36
+ raise FileNotFoundError(f"File not found: {kb_path}")
37
+ if not kb_latest_path.exists():
38
+ raise FileNotFoundError(f"File not found: {kb_latest_path}")
39
+
40
+ self.knowledge_base = pd.read_csv(kb_path)
41
+ self.knowledge_base_latest = pd.read_csv(kb_latest_path)
42
+
43
+ # Extract unique values
44
+ self.ALL_PROD_NAMES = self.knowledge_base['prod_name'].values
45
+ self.ALL_ARTIST_NAMES = self.knowledge_base['artist'].values
46
+ self.ALL_SET_NAMES = self.knowledge_base['set_name'].values
47
+
48
+ print(f"✓ Successfully loaded {len(self.knowledge_base)} records from knowledge_base")
49
+ print(f"✓ Successfully loaded {len(self.knowledge_base_latest)} records from knowledge_base_latest")
50
+ print(f"✓ Found {len(self.ALL_SET_NAMES)} unique sets")
51
+
52
+ except Exception as e:
53
+ print(f"❌ ERROR loading data: {e}")
54
+ print(f"Current working directory: {os.getcwd()}")
55
+ print(f"Files in data directory: {list(data_dir.glob('*.csv')) if data_dir.exists() else 'Directory not found'}")
56
+
57
+ # Initialize empty DataFrames as fallback
58
+ self.knowledge_base = pd.DataFrame()
59
+ self.knowledge_base_latest = pd.DataFrame()
60
+ self.ALL_PROD_NAMES = []
61
+ self.ALL_ARTIST_NAMES = []
62
+ self.ALL_SET_NAMES = []
63
+
64
+ raise RuntimeError(f"Failed to load Pokemon card data: {e}")
65
 
66
  def get_data_shape(self) -> int:
67
  """
68
  Retrieves the number of set names.
69
  Use this when user asks about the number of unique sets.
70
  Returns:
71
+ int: length of knowledge base
72
  """
73
  return len(self.ALL_SET_NAMES)
74
 
 
82
  Returns:
83
  list: A list of 'prod_name's that matches the 'name_query'
84
  """
85
+ if len(self.ALL_PROD_NAMES) == 0:
86
+ return {"error": "Data not loaded."}
87
 
88
  prod_names_match = process.extract(name_query, self.ALL_PROD_NAMES, scorer=fuzz.WRatio, limit=5)
89
  return [name[0].replace("_", " ") for name in prod_names_match]
 
103
  dict: A dictionary containing 'used_price', 'graded_price', 'trend_6',
104
  and other key metrics. Returns an 'error' key if not found.
105
  """
106
+ if len(self.ALL_PROD_NAMES) == 0:
107
+ return {"error": "Data not loaded."}
108
 
109
  match = process.extractOne(name_query, self.ALL_PROD_NAMES, scorer=fuzz.WRatio)
110
  if not match or match[1] < 70:
 
114
  card_df = self.knowledge_base_latest[self.knowledge_base_latest["prod_name"] == prod_name]
115
 
116
  if card_df.empty:
117
+ return {"error": f"Data missing for '{prod_name}'."}
118
 
119
  return card_df.to_dict(orient="records")[0]
120
 
 
121
  def find_grading_opportunities(self, max_price: float = 100, min_profit: float = 20) -> list:
122
  """
123
  Scans the market for 'Arbitrage' opportunities where the gap between the Raw
 
139
  min_profit_grades = profitable_grades[profitable_grades["grade_profit"] >= min_profit]
140
  min_profit_grades = min_profit_grades.sort_values(
141
  by="grade_profit", ascending=False
142
+ ).head(10)
143
  output_columns = [
144
  "prod_name",
145
  "used_price",
 
152
  min_profit_grades = min_profit_grades[output_columns]
153
  return min_profit_grades.to_dict(orient="records")
154
 
155
+ def get_market_movers(self, sort_by: str = "uptrend", interval: int = 6, market_type: str = "used") -> list:
156
  """
157
  Identifies cards with the strongest positive or negative price trends over a sustained period (3 or 6 months).
158
 
 
169
  Returns:
170
  list: A list of the top 10 cards matching the trend criteria, including their percentage change.
171
  """
172
+ market_move_data = self.knowledge_base_latest.sort_values(
173
+ by=f"{market_type}_trend_{interval}",
174
+ ascending=(sort_by != "uptrend")
175
+ ).head(10)
176
+ output_columns = ["prod_name", "used_price", "graded_price", f"{market_type}_trend_{interval}"]
177
  market_move_data = market_move_data[output_columns]
178
  return market_move_data.to_dict(orient="records")
179
 
 
205
  return {"error": "Invalid 'interval' value. Must be 3 or 6."}
206
 
207
  card_info = self.get_card_info(card_name)
208
+ if "error" in card_info:
209
+ return card_info
210
 
211
  if interval not in [3, 6]:
212
  return {"error": f"Invalid interval requested: {interval}. Only 3 or 6 months are supported."}
 
230
  graded_volatility = card_info.get("graded_vol_6")
231
 
232
  if used_volatility is None or graded_volatility is None:
233
+ return {"error": f"Volatility data missing for {card_name} at {interval} months."}
234
 
235
  return {
236
+ "used_volatility": used_volatility,
237
+ "graded_volatility": graded_volatility,
238
+ f"used_volatility_assessment_{interval}_months": self._calculate_risk_label(used_volatility, used_vol_low_threshold, used_vol_high_threshold),
239
+ f"graded_volatility_assessment_{interval}_months": self._calculate_risk_label(graded_volatility, graded_vol_low_threshold, graded_vol_high_threshold),
240
  }
241
 
242
  def get_roi_metrics(self, card_name: str) -> dict:
 
252
  dict: Returns 3-month and 6-month ROI percentages for both Used and Graded conditions.
253
  """
254
  card_info = self.get_card_info(card_name)
255
+ if "error" in card_info:
256
+ return card_info
257
+
258
  return {
259
  "used_price": card_info.get("used_price"),
260
  "used_return_3_months": card_info.get("used_return_3"),
 
288
  output_columns = ["prod_name", "set_name", "graded_price"]
289
  return jump_data[output_columns].to_dict(orient="records")
290
 
 
291
  else:
292
  return {"error": f"Invalid market_type '{market_type}'. Please use 'used' or 'graded'."}
293
 
 
298
  Use this for "Niche" requests or when users ask about art styles.
299
 
300
  Args:
301
+ artist_name (str): The artist's name.
302
 
303
  Returns:
304
  dict: A list of cards by that artist, sorted by profitability.
305
  """
306
+ if len(self.ALL_ARTIST_NAMES) == 0:
307
+ return {"error": "Data not loaded."}
308
+
309
  artist_match = process.extractOne(artist_name, self.ALL_ARTIST_NAMES, scorer=fuzz.WRatio)
310
 
311
  if not artist_match or artist_match[1] < 75:
 
324
  "grade_profit_ratio"
325
  ]
326
 
 
327
  if profitable_cards.empty:
328
  return {"result": f"No currently profitable cards found by artist {artist_name_match} in the latest data."}
329
 
 
340
  rather than specific cards.
341
 
342
  Args:
343
+ set_name (str): The name of the set (e.g., "Evolving Skies"). Fuzzy matched.
344
 
345
  Returns:
346
  dict: Average trends, average profitability, and the set's 'Chase Card'.
347
+ """
348
+ if len(self.ALL_SET_NAMES) == 0:
349
+ return {"error": "Data not loaded."}
350
+
351
+ set_match = process.extractOne(set_name, self.ALL_SET_NAMES, scorer=fuzz.WRatio)
352
+
353
+ if not set_match or set_match[1] < 70:
354
+ return {"error": f"Set '{set_name}' not found. Available sets: {list(self.ALL_SET_NAMES[:5])}"}
355
+
356
+ set_name_match = set_match[0]
357
  set_card_data = self.knowledge_base_latest[self.knowledge_base_latest["set_name"] == set_name_match]
358
+
359
+ if set_card_data.empty:
360
+ return {"error": f"No data found for set: {set_name_match}"}
361
+
362
  total_cards = len(set_card_data)
363
  avg_trend_6 = set_card_data["used_trend_6"].mean()
364
  avg_grade_profit = set_card_data["grade_profit"].mean()
365
  chase_card_row = set_card_data.sort_values('used_price', ascending=False).iloc[0]
366
 
367
  return {
368
+ "set_name": set_name_match.replace("_", " "),
369
  "total_cards_tracked": total_cards,
370
  "market_sentiment_6mo": f"{avg_trend_6:.2f}%",
371
  "avg_grading_profit": f"${avg_grade_profit:.2f}",
372
  "chase_card": chase_card_row['prod_name'],
373
+ "chase_card_price": f"${chase_card_row['used_price']:.2f}"
374
  }