kshitijthakkar commited on
Commit
739f384
·
1 Parent(s): f42b8e7

fix: Resolve compare screen and MCP connection issues

Browse files

- Fix compare screen timestamp format mismatch by preventing dataframe mutation in create_trends_plot()
- Fix MCP server ClosedResourceError by properly managing ClientSession lifecycle
- Add automatic reconnection mechanism with retry logic for MCP tool calls
- Improve connection cleanup and error handling

Fixes two critical issues:
1. Compare screen showing "data not in leaderboard" error due to pandas timestamp conversion
2. Agent chat screen failing with ClosedResourceError when accessing leaderboard resource

components/analytics_charts.py CHANGED
@@ -662,6 +662,9 @@ def create_trends_plot(df: pd.DataFrame) -> go.Figure:
662
  from plotly.subplots import make_subplots
663
 
664
  try:
 
 
 
665
  # Use evaluation_date or timestamp depending on what's available
666
  date_col = 'evaluation_date' if 'evaluation_date' in df.columns else 'timestamp'
667
 
 
662
  from plotly.subplots import make_subplots
663
 
664
  try:
665
+ # Create a copy to prevent mutating the input dataframe
666
+ df = df.copy()
667
+
668
  # Use evaluation_date or timestamp depending on what's available
669
  date_col = 'evaluation_date' if 'evaluation_date' in df.columns else 'timestamp'
670
 
mcp_client/client.py CHANGED
@@ -28,6 +28,8 @@ class MCPClient:
28
  )
29
  self.session: Optional[ClientSession] = None
30
  self._initialized = False
 
 
31
 
32
  async def initialize(self):
33
  """Initialize connection to MCP server"""
@@ -35,24 +37,52 @@ class MCPClient:
35
  return
36
 
37
  try:
38
- # Connect to SSE endpoint
39
- async with sse_client(self.server_url) as (read, write):
40
- async with ClientSession(read, write) as session:
41
- self.session = session
42
- await session.initialize()
43
- self._initialized = True
44
-
45
- # List available tools for verification
46
- tools_result = await session.list_tools()
47
- print(f"✅ Connected to TraceMind MCP Server at {self.server_url}")
48
- print(f"📊 Available tools: {len(tools_result.tools)}")
49
- for tool in tools_result.tools:
50
- print(f" - {tool.name}: {tool.description}")
 
 
 
51
 
52
  except Exception as e:
53
  print(f"❌ Failed to connect to MCP server: {e}")
 
 
54
  raise
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  async def analyze_leaderboard(
57
  self,
58
  leaderboard_repo: str = "kshitijthakkar/smoltrace-leaderboard",
@@ -76,9 +106,6 @@ class MCPClient:
76
  Returns:
77
  AI-generated analysis of the leaderboard
78
  """
79
- if not self._initialized:
80
- await self.initialize()
81
-
82
  try:
83
  # Build arguments
84
  args = {
@@ -94,8 +121,8 @@ class MCPClient:
94
  if gemini_api_key:
95
  args["gemini_api_key"] = gemini_api_key
96
 
97
- # Call MCP tool
98
- result = await self.session.call_tool("analyze_leaderboard", arguments=args)
99
 
100
  # Extract text from result
101
  if result.content and len(result.content) > 0:
@@ -127,9 +154,6 @@ class MCPClient:
127
  Returns:
128
  AI-generated answer to the trace question
129
  """
130
- if not self._initialized:
131
- await self.initialize()
132
-
133
  try:
134
  args = {
135
  "trace_data": trace_data,
@@ -143,7 +167,7 @@ class MCPClient:
143
  if gemini_api_key:
144
  args["gemini_api_key"] = gemini_api_key
145
 
146
- result = await self.session.call_tool("debug_trace", arguments=args)
147
 
148
  if result.content and len(result.content) > 0:
149
  return result.content[0].text
@@ -176,9 +200,6 @@ class MCPClient:
176
  Returns:
177
  Cost estimation with breakdown
178
  """
179
- if not self._initialized:
180
- await self.initialize()
181
-
182
  try:
183
  args = {
184
  "model": model,
@@ -193,7 +214,7 @@ class MCPClient:
193
  if gemini_api_key:
194
  args["gemini_api_key"] = gemini_api_key
195
 
196
- result = await self.session.call_tool("estimate_cost", arguments=args)
197
 
198
  if result.content and len(result.content) > 0:
199
  return result.content[0].text
@@ -222,9 +243,6 @@ class MCPClient:
222
  Returns:
223
  AI-generated comparison analysis
224
  """
225
- if not self._initialized:
226
- await self.initialize()
227
-
228
  try:
229
  args = {
230
  "run_data_list": run_data_list
@@ -237,7 +255,7 @@ class MCPClient:
237
  if gemini_api_key:
238
  args["gemini_api_key"] = gemini_api_key
239
 
240
- result = await self.session.call_tool("compare_runs", arguments=args)
241
 
242
  if result.content and len(result.content) > 0:
243
  return result.content[0].text
@@ -266,9 +284,6 @@ class MCPClient:
266
  Returns:
267
  AI-generated results analysis with recommendations
268
  """
269
- if not self._initialized:
270
- await self.initialize()
271
-
272
  try:
273
  args = {
274
  "results_data": results_data,
@@ -280,7 +295,7 @@ class MCPClient:
280
  if gemini_api_key:
281
  args["gemini_api_key"] = gemini_api_key
282
 
283
- result = await self.session.call_tool("analyze_results", arguments=args)
284
 
285
  if result.content and len(result.content) > 0:
286
  return result.content[0].text
@@ -307,9 +322,6 @@ class MCPClient:
307
  Returns:
308
  Dataset information and structure
309
  """
310
- if not self._initialized:
311
- await self.initialize()
312
-
313
  try:
314
  args = {
315
  "dataset_repo": dataset_repo
@@ -320,7 +332,7 @@ class MCPClient:
320
  if gemini_api_key:
321
  args["gemini_api_key"] = gemini_api_key
322
 
323
- result = await self.session.call_tool("get_dataset", arguments=args)
324
 
325
  if result.content and len(result.content) > 0:
326
  return result.content[0].text
@@ -330,13 +342,28 @@ class MCPClient:
330
  except Exception as e:
331
  return f"❌ Error calling get_dataset: {str(e)}"
332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  async def close(self):
334
  """Close the MCP client session"""
335
- if self.session:
336
- # Note: ClientSession doesn't have an explicit close method
337
- # The context manager handles cleanup
338
- self.session = None
339
- self._initialized = False
340
 
341
 
342
  # Singleton instance for use across the app
 
28
  )
29
  self.session: Optional[ClientSession] = None
30
  self._initialized = False
31
+ self._sse_context = None
32
+ self._session_context = None
33
 
34
  async def initialize(self):
35
  """Initialize connection to MCP server"""
 
37
  return
38
 
39
  try:
40
+ # Connect to SSE endpoint and keep it open
41
+ self._sse_context = sse_client(self.server_url)
42
+ read, write = await self._sse_context.__aenter__()
43
+
44
+ # Create session and keep it open
45
+ self._session_context = ClientSession(read, write)
46
+ self.session = await self._session_context.__aenter__()
47
+ await self.session.initialize()
48
+ self._initialized = True
49
+
50
+ # List available tools for verification
51
+ tools_result = await self.session.list_tools()
52
+ print(f" Connected to TraceMind MCP Server at {self.server_url}")
53
+ print(f"📊 Available tools: {len(tools_result.tools)}")
54
+ for tool in tools_result.tools:
55
+ print(f" - {tool.name}: {tool.description}")
56
 
57
  except Exception as e:
58
  print(f"❌ Failed to connect to MCP server: {e}")
59
+ # Clean up on error
60
+ await self._cleanup_connections()
61
  raise
62
 
63
+ async def _ensure_connected(self):
64
+ """Ensure the connection is active, reconnect if needed"""
65
+ if not self._initialized or self.session is None:
66
+ print("🔄 Reconnecting to MCP server...")
67
+ await self._cleanup_connections()
68
+ await self.initialize()
69
+
70
+ async def _call_tool_with_retry(self, tool_name: str, arguments: dict, max_retries: int = 2):
71
+ """Call MCP tool with automatic retry on connection errors"""
72
+ for attempt in range(max_retries):
73
+ try:
74
+ await self._ensure_connected()
75
+ result = await self.session.call_tool(tool_name, arguments=arguments)
76
+ return result
77
+ except Exception as e:
78
+ error_str = str(e)
79
+ if "ClosedResourceError" in error_str or "closed" in error_str.lower():
80
+ if attempt < max_retries - 1:
81
+ print(f"⚠️ Connection lost, retrying... (attempt {attempt + 1}/{max_retries})")
82
+ await self._cleanup_connections()
83
+ continue
84
+ raise
85
+
86
  async def analyze_leaderboard(
87
  self,
88
  leaderboard_repo: str = "kshitijthakkar/smoltrace-leaderboard",
 
106
  Returns:
107
  AI-generated analysis of the leaderboard
108
  """
 
 
 
109
  try:
110
  # Build arguments
111
  args = {
 
121
  if gemini_api_key:
122
  args["gemini_api_key"] = gemini_api_key
123
 
124
+ # Call MCP tool with retry
125
+ result = await self._call_tool_with_retry("analyze_leaderboard", args)
126
 
127
  # Extract text from result
128
  if result.content and len(result.content) > 0:
 
154
  Returns:
155
  AI-generated answer to the trace question
156
  """
 
 
 
157
  try:
158
  args = {
159
  "trace_data": trace_data,
 
167
  if gemini_api_key:
168
  args["gemini_api_key"] = gemini_api_key
169
 
170
+ result = await self._call_tool_with_retry("debug_trace", args)
171
 
172
  if result.content and len(result.content) > 0:
173
  return result.content[0].text
 
200
  Returns:
201
  Cost estimation with breakdown
202
  """
 
 
 
203
  try:
204
  args = {
205
  "model": model,
 
214
  if gemini_api_key:
215
  args["gemini_api_key"] = gemini_api_key
216
 
217
+ result = await self._call_tool_with_retry("estimate_cost", args)
218
 
219
  if result.content and len(result.content) > 0:
220
  return result.content[0].text
 
243
  Returns:
244
  AI-generated comparison analysis
245
  """
 
 
 
246
  try:
247
  args = {
248
  "run_data_list": run_data_list
 
255
  if gemini_api_key:
256
  args["gemini_api_key"] = gemini_api_key
257
 
258
+ result = await self._call_tool_with_retry("compare_runs", args)
259
 
260
  if result.content and len(result.content) > 0:
261
  return result.content[0].text
 
284
  Returns:
285
  AI-generated results analysis with recommendations
286
  """
 
 
 
287
  try:
288
  args = {
289
  "results_data": results_data,
 
295
  if gemini_api_key:
296
  args["gemini_api_key"] = gemini_api_key
297
 
298
+ result = await self._call_tool_with_retry("analyze_results", args)
299
 
300
  if result.content and len(result.content) > 0:
301
  return result.content[0].text
 
322
  Returns:
323
  Dataset information and structure
324
  """
 
 
 
325
  try:
326
  args = {
327
  "dataset_repo": dataset_repo
 
332
  if gemini_api_key:
333
  args["gemini_api_key"] = gemini_api_key
334
 
335
+ result = await self._call_tool_with_retry("get_dataset", args)
336
 
337
  if result.content and len(result.content) > 0:
338
  return result.content[0].text
 
342
  except Exception as e:
343
  return f"❌ Error calling get_dataset: {str(e)}"
344
 
345
+ async def _cleanup_connections(self):
346
+ """Internal helper to clean up connections"""
347
+ if self._session_context:
348
+ try:
349
+ await self._session_context.__aexit__(None, None, None)
350
+ except Exception as e:
351
+ print(f"⚠️ Error closing session context: {e}")
352
+ self._session_context = None
353
+ self.session = None
354
+
355
+ if self._sse_context:
356
+ try:
357
+ await self._sse_context.__aexit__(None, None, None)
358
+ except Exception as e:
359
+ print(f"⚠️ Error closing SSE context: {e}")
360
+ self._sse_context = None
361
+
362
+ self._initialized = False
363
+
364
  async def close(self):
365
  """Close the MCP client session"""
366
+ await self._cleanup_connections()
 
 
 
 
367
 
368
 
369
  # Singleton instance for use across the app