Nyha15 commited on
Commit
88eb801
·
1 Parent(s): 60fbe6c

Added url field for dataset

Browse files
Files changed (1) hide show
  1. app.py +146 -348
app.py CHANGED
@@ -1,27 +1,33 @@
1
  """
2
- Data Analyst Duo MCP Implementation - Simplified version
 
 
 
 
3
  """
4
 
5
  import os
6
  import json
7
  import datetime
8
- import gradio as gr
 
 
 
9
  import pandas as pd
10
  import numpy as np
11
  import requests
12
- from io import StringIO
13
- import logging
14
- import uuid
15
 
16
  # Configure logging
17
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
 
 
 
18
  logger = logging.getLogger(__name__)
19
 
20
  # ============== MCP Protocol Implementation ==============
21
 
22
  class MCPMessage:
23
- """Base class for MCP messages that agents exchange"""
24
-
25
  def __init__(self, sender, message_type, content):
26
  self.id = str(uuid.uuid4())
27
  self.sender = sender
@@ -38,10 +44,7 @@ class MCPMessage:
38
  "timestamp": self.timestamp
39
  }
40
 
41
-
42
  class MCPTool:
43
- """Defines a tool that can be used by agents through the MCP protocol"""
44
-
45
  def __init__(self, name, description, function):
46
  self.name = name
47
  self.description = description
@@ -50,10 +53,7 @@ class MCPTool:
50
  def execute(self, params):
51
  return self.function(params)
52
 
53
-
54
  class MCPAgent:
55
- """Base agent class implementing MCP protocol"""
56
-
57
  def __init__(self, name, description):
58
  self.name = name
59
  self.description = description
@@ -63,427 +63,225 @@ class MCPAgent:
63
  self.message_history = []
64
 
65
  def register_tool(self, tool):
66
- """Register a tool that this agent can use"""
67
  self.tools[tool.name] = tool
68
 
69
  def connect(self, peer):
70
- """Connect to another agent as a peer"""
71
  self.peers[peer.name] = peer
72
 
73
  def send_message(self, receiver, message_type, content):
74
- """Send a message to a peer agent"""
75
  if receiver not in self.peers:
76
  raise ValueError(f"Peer {receiver} not found")
77
-
78
- message = MCPMessage(self.name, message_type, content)
79
- message_dict = message.to_dict()
80
-
81
- # Save to message history
82
- self.message_history.append({
83
- "type": "sent",
84
- "message": message_dict
85
- })
86
-
87
- # Send to receiver
88
- self.peers[receiver].receive_message(message)
89
- logger.info(f"Agent {self.name} sent {message_type} to {receiver}")
90
- return message_dict
91
 
92
  def receive_message(self, message):
93
- """Receive a message from a peer agent"""
94
  self.message_queue.append(message)
95
-
96
- # Save to message history
97
- self.message_history.append({
98
- "type": "received",
99
- "message": message.to_dict()
100
- })
101
-
102
- logger.info(f"Agent {self.name} received {message.message_type} from {message.sender}")
103
 
104
  def process_messages(self):
105
- """Process all messages in the queue"""
106
- processed = []
107
  while self.message_queue:
108
- message = self.message_queue.pop(0)
109
- response = self.handle_message(message)
110
- processed.append(response)
111
- return processed
112
-
113
- def handle_message(self, message):
114
- """Handle a message - to be implemented by subclasses"""
115
- raise NotImplementedError("Subclasses must implement handle_message")
116
 
117
  def get_message_history(self):
118
- """Get the agent's message history"""
119
  return self.message_history
120
 
 
 
121
 
122
- # ============== Compute Agent Implementation ==============
123
 
124
  class ComputeAgent(MCPAgent):
125
- """Agent responsible for data loading, cleaning, and computation"""
126
-
127
  def __init__(self, name="ComputeAgent"):
128
- super().__init__(name, "Agent responsible for data loading, cleaning and computation")
129
  self.dataframe = None
130
 
131
- # Register tools
132
- self.register_tool(MCPTool(
133
- "load_dataset",
134
- "Load a dataset from URL",
135
- self._load_dataset
136
- ))
137
-
138
- self.register_tool(MCPTool(
139
- "compute_statistics",
140
- "Compute basic statistics on the dataset",
141
- self._compute_statistics
142
- ))
143
-
144
- self.register_tool(MCPTool(
145
- "compute_correlation",
146
- "Compute correlation between columns",
147
- self._compute_correlation
148
- ))
149
 
150
  def _load_dataset(self, params):
151
- """Load a dataset from URL"""
152
- dataset_url = params.get("url")
153
-
 
154
  try:
155
- # Use default cereals dataset if not specified
156
- if not dataset_url or dataset_url == "default":
157
- dataset_url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/cereal.csv"
158
-
159
- # Load the dataset
160
- response = requests.get(dataset_url)
161
- content = response.content.decode('utf-8')
162
- self.dataframe = pd.read_csv(StringIO(content))
163
-
164
- # Basic info about the dataset
165
- info = {
166
  "status": "success",
167
- "rows": len(self.dataframe),
168
  "columns": list(self.dataframe.columns),
169
  "preview": self.dataframe.head(5).to_dict(orient="records")
170
  }
171
-
172
- return info
173
-
174
  except Exception as e:
 
175
  return {"status": "error", "message": str(e)}
176
 
177
  def _compute_statistics(self, params):
178
- """Compute basic statistics on the dataset"""
179
  if self.dataframe is None:
180
  return {"status": "error", "message": "No dataset loaded"}
181
-
182
  try:
183
- # Get columns to compute stats for
184
- columns = params.get("columns", list(self.dataframe.select_dtypes(include=[np.number]).columns))
185
-
186
- # Basic descriptive statistics
187
- stats = self.dataframe[columns].describe().to_dict()
188
-
189
- return {
190
- "status": "success",
191
- "statistics": stats
192
- }
193
-
194
  except Exception as e:
 
195
  return {"status": "error", "message": str(e)}
196
 
197
  def _compute_correlation(self, params):
198
- """Compute correlation between columns"""
199
  if self.dataframe is None:
200
  return {"status": "error", "message": "No dataset loaded"}
201
-
202
  try:
203
- # Get columns to compute correlation for
204
- columns = params.get("columns", list(self.dataframe.select_dtypes(include=[np.number]).columns))
205
-
206
- corr_matrix = self.dataframe[columns].corr().to_dict()
207
-
208
- return {
209
- "status": "success",
210
- "correlation_matrix": corr_matrix
211
- }
212
-
213
  except Exception as e:
 
214
  return {"status": "error", "message": str(e)}
215
 
216
  def handle_message(self, message):
217
- """Handle incoming messages from other agents"""
218
- if message.message_type == "request_data_load":
219
- result = self._load_dataset(message.content)
 
220
  return self.send_message(message.sender, "data_load_result", result)
221
-
222
- elif message.message_type == "request_statistics":
223
- result = self._compute_statistics(message.content)
224
  return self.send_message(message.sender, "statistics_result", result)
225
-
226
- elif message.message_type == "request_correlation":
227
- result = self._compute_correlation(message.content)
228
  return self.send_message(message.sender, "correlation_result", result)
229
-
230
  else:
231
- return {"status": "error", "message": f"Unknown message type: {message.message_type}"}
232
-
233
 
234
- # ============== Interpret Agent Implementation ==============
235
 
236
  class InterpretAgent(MCPAgent):
237
- """Agent responsible for interpreting results and visualizing data"""
238
-
239
  def __init__(self, name="InterpretAgent"):
240
- super().__init__(name, "Agent responsible for interpreting results and visualizing data")
241
  self.dataset_info = None
242
  self.statistics = None
243
- self.correlation_data = None
244
-
245
- # Register tools
246
- self.register_tool(MCPTool(
247
- "interpret_statistics",
248
- "Interpret statistical results and provide insights",
249
- self._interpret_statistics
250
- ))
251
-
252
- self.register_tool(MCPTool(
253
- "interpret_correlation",
254
- "Interpret correlation results and provide insights",
255
- self._interpret_correlation
256
- ))
257
-
258
- self.register_tool(MCPTool(
259
- "generate_report",
260
- "Generate a report with key findings",
261
- self._generate_report
262
- ))
263
-
264
- def _interpret_statistics(self, params):
265
- """Interpret statistical results and provide insights"""
266
- if not self.statistics:
267
- return {"status": "error", "message": "No statistics data available"}
268
-
269
- try:
270
- insights = []
271
- stats = self.statistics.get("statistics", {})
272
-
273
- # Simple rule-based insights
274
- for col, col_stats in stats.items():
275
- # Add a simple insight about the mean value
276
- if "mean" in col_stats:
277
- insights.append(f"The average {col} is {col_stats['mean']:.2f}")
278
 
279
- # Add insight about range
280
- if "min" in col_stats and "max" in col_stats:
281
- insights.append(f"{col} ranges from {col_stats['min']:.2f} to {col_stats['max']:.2f}")
282
 
283
- return {
284
- "status": "success",
285
- "insights": insights[:3], # Limit to top 3 insights
286
- "summary": "Statistical analysis complete."
287
- }
288
-
289
- except Exception as e:
290
- return {"status": "error", "message": str(e)}
 
291
 
292
  def _interpret_correlation(self, params):
293
- """Interpret correlation results and provide insights"""
294
- if not self.correlation_data:
295
- return {"status": "error", "message": "No correlation data available"}
296
-
297
- try:
298
- insights = ["Correlation analysis complete."]
299
-
300
- return {
301
- "status": "success",
302
- "insights": insights,
303
- "summary": "Correlation analysis complete."
304
- }
305
-
306
- except Exception as e:
307
- return {"status": "error", "message": str(e)}
308
 
309
  def _generate_report(self, params):
310
- """Generate a report with key findings"""
311
- try:
312
- report_sections = []
313
-
314
- # Dataset overview
315
- if self.dataset_info:
316
- report_sections.append({
317
- "title": "Dataset Overview",
318
- "content": f"The dataset contains {self.dataset_info.get('rows', 0)} rows and {len(self.dataset_info.get('columns', []))} columns."
319
- })
320
-
321
- # Simple conclusion
322
- report_sections.append({
323
- "title": "Conclusions",
324
- "content": "Analysis complete."
325
  })
326
-
327
- return {
328
- "status": "success",
329
- "report": {
330
- "title": params.get("report_title", "Data Analysis Report"),
331
- "sections": report_sections
332
- }
333
  }
334
-
335
- except Exception as e:
336
- return {"status": "error", "message": str(e)}
337
 
338
  def handle_message(self, message):
339
- """Handle incoming messages from other agents"""
340
- if message.message_type == "data_load_result":
341
- self.dataset_info = message.content
342
- return self.send_message(message.sender, "acknowledge", {"status": "received", "message": "Dataset info received"})
343
-
344
- elif message.message_type == "statistics_result":
345
- self.statistics = message.content
346
- insights = self._interpret_statistics({})
347
- return self.send_message(message.sender, "statistics_interpretation", insights)
348
-
349
- elif message.message_type == "correlation_result":
350
- self.correlation_data = message.content
351
- insights = self._interpret_correlation({})
352
- return self.send_message(message.sender, "correlation_interpretation", insights)
353
-
354
- elif message.message_type == "request_report":
355
- report = self._generate_report(message.content)
356
  return self.send_message(message.sender, "report_result", report)
357
-
358
  else:
359
- return {"status": "error", "message": f"Unknown message type: {message.message_type}"}
360
 
361
-
362
- # ============== Main Analysis Workflow ==============
363
 
364
  class DataAnalystDuo:
365
- """Main class for the Data Analyst Duo MCP implementation"""
366
-
367
  def __init__(self):
368
  self.compute_agent = ComputeAgent()
369
  self.interpret_agent = InterpretAgent()
370
-
371
- # Connect the agents as peers
372
  self.compute_agent.connect(self.interpret_agent)
373
  self.interpret_agent.connect(self.compute_agent)
374
 
375
  def run_analysis(self, dataset_url="default"):
376
- """Run the complete analysis workflow"""
377
-
378
- # 1. Load dataset
379
  self.interpret_agent.send_message("ComputeAgent", "request_data_load", {"url": dataset_url})
380
- self.compute_agent.process_messages()
381
- self.interpret_agent.process_messages()
382
-
383
- # 2. Compute statistics
384
- self.interpret_agent.send_message("ComputeAgent", "request_statistics", {"descriptive": True})
385
- self.compute_agent.process_messages()
386
- self.interpret_agent.process_messages()
387
-
388
- # 3. Compute correlation
389
- self.interpret_agent.send_message("ComputeAgent", "request_correlation", {"method": "pearson"})
390
- self.compute_agent.process_messages()
391
- self.interpret_agent.process_messages()
392
-
393
- # 4. Generate final report
394
- self.compute_agent.send_message("InterpretAgent", "request_report", {"report_title": "Data Analysis Report"})
395
- self.interpret_agent.process_messages()
396
- self.compute_agent.process_messages()
397
-
398
- # Collect results
399
- results = {
400
- "compute_agent_messages": self.compute_agent.get_message_history(),
401
- "interpret_agent_messages": self.interpret_agent.get_message_history()
402
- }
403
-
404
- return results
405
 
 
 
 
 
406
 
407
  # ============== Gradio Interface ==============
408
 
409
- def format_json(json_data):
410
- """Format JSON data for display"""
411
- if isinstance(json_data, dict) or isinstance(json_data, list):
412
- return json.dumps(json_data, indent=2)
413
- return str(json_data)
414
 
415
  def run_analysis(dataset_url):
416
- """Run the data analysis workflow and return formatted messages"""
417
- try:
418
- # Use default cereals dataset if not specified
419
- if not dataset_url:
420
- dataset_url = "default"
421
-
422
- # Create and run the analyst duo
423
- duo = DataAnalystDuo()
424
- results = duo.run_analysis(dataset_url)
425
-
426
- # Format messages for display
427
- all_messages = []
428
-
429
- # Add compute agent messages
430
- for msg in results["compute_agent_messages"]:
431
- formatted_msg = f"[{msg['message']['timestamp']}] ComputeAgent {msg['type'].upper()} - Type: {msg['message']['message_type']}\n"
432
- formatted_msg += format_json(msg['message']['content'])
433
- formatted_msg += "\n\n" + "-"*80 + "\n\n"
434
- all_messages.append((msg['message']['timestamp'], formatted_msg))
435
-
436
- # Add interpret agent messages
437
- for msg in results["interpret_agent_messages"]:
438
- formatted_msg = f"[{msg['message']['timestamp']}] InterpretAgent {msg['type'].upper()} - Type: {msg['message']['message_type']}\n"
439
- formatted_msg += format_json(msg['message']['content'])
440
- formatted_msg += "\n\n" + "-"*80 + "\n\n"
441
- all_messages.append((msg['message']['timestamp'], formatted_msg))
442
-
443
- # Sort messages by timestamp
444
- all_messages.sort(key=lambda x: x[0])
445
-
446
- # Join messages
447
- formatted_output = "\n".join([msg[1] for msg in all_messages])
448
-
449
- return formatted_output
450
-
451
- except Exception as e:
452
- import traceback
453
- return f"Error: {str(e)}\n\n{traceback.format_exc()}"
454
-
455
- # Define the Gradio interface
456
- with gr.Blocks(title="Data Analyst Duo - MCP Communication") as app:
457
- gr.Markdown("""
458
- # Data Analyst Duo - Model Context Protocol (MCP) Implementation
459
-
460
- This application demonstrates a multi-agent system using the Model Context Protocol (MCP).
461
- It consists of two agents:
462
-
463
- 1. **ComputeAgent**: Responsible for data loading, cleaning, and computation
464
- 2. **InterpretAgent**: Responsible for interpreting results
465
-
466
- The agents communicate directly using standardized MCP messages, showcasing agent-to-agent communication.
467
- """)
468
-
469
- dataset_url = gr.Textbox(label="Dataset URL (leave empty for default cereals dataset)", placeholder="Enter dataset URL or leave empty for default")
470
- run_button = gr.Button("Run Analysis")
471
- mcp_messages = gr.Textbox(label="MCP Message Flow", lines=30)
472
-
473
- run_button.click(fn=run_analysis, inputs=dataset_url, outputs=mcp_messages)
474
-
475
- gr.Markdown("""
476
- ## How This Demonstrates MCP
477
-
478
- This application shows the Model Context Protocol in action:
479
-
480
- 1. **Standardized Message Structure**: All communication between agents follows a consistent format
481
- 2. **Direct Peer Communication**: Agents communicate directly with structured messages
482
- 3. **Asynchronous Processing**: Each agent processes messages independently
483
-
484
- The message flow display shows the exact JSON messages exchanged between agents, demonstrating the protocol in action.
485
- """)
486
 
487
- # Launch the app
488
  if __name__ == "__main__":
489
- app.launch()
 
1
  """
2
+ Data Analyst Duo MCP Implementation - Full Working Version
3
+ Supports loading any CSV over HTTP(S), including:
4
+ - Default cereal dataset
5
+ - Seaborn diamonds.csv
6
+ - FiveThirtyEight candy-data.csv
7
  """
8
 
9
  import os
10
  import json
11
  import datetime
12
+ import logging
13
+ import uuid
14
+ from io import StringIO
15
+
16
  import pandas as pd
17
  import numpy as np
18
  import requests
19
+ import gradio as gr
 
 
20
 
21
  # Configure logging
22
+ logging.basicConfig(
23
+ level=logging.INFO,
24
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
25
+ )
26
  logger = logging.getLogger(__name__)
27
 
28
  # ============== MCP Protocol Implementation ==============
29
 
30
  class MCPMessage:
 
 
31
  def __init__(self, sender, message_type, content):
32
  self.id = str(uuid.uuid4())
33
  self.sender = sender
 
44
  "timestamp": self.timestamp
45
  }
46
 
 
47
  class MCPTool:
 
 
48
  def __init__(self, name, description, function):
49
  self.name = name
50
  self.description = description
 
53
  def execute(self, params):
54
  return self.function(params)
55
 
 
56
  class MCPAgent:
 
 
57
  def __init__(self, name, description):
58
  self.name = name
59
  self.description = description
 
63
  self.message_history = []
64
 
65
  def register_tool(self, tool):
 
66
  self.tools[tool.name] = tool
67
 
68
  def connect(self, peer):
 
69
  self.peers[peer.name] = peer
70
 
71
  def send_message(self, receiver, message_type, content):
 
72
  if receiver not in self.peers:
73
  raise ValueError(f"Peer {receiver} not found")
74
+ msg = MCPMessage(self.name, message_type, content)
75
+ self.message_history.append({"type": "sent", "message": msg.to_dict()})
76
+ self.peers[receiver].receive_message(msg)
77
+ logger.info(f"{self.name} → {receiver}: {message_type}")
78
+ return msg.to_dict()
 
 
 
 
 
 
 
 
 
79
 
80
  def receive_message(self, message):
 
81
  self.message_queue.append(message)
82
+ self.message_history.append({"type": "received", "message": message.to_dict()})
83
+ logger.info(f"{self.name} received {message.message_type} from {message.sender}")
 
 
 
 
 
 
84
 
85
  def process_messages(self):
86
+ responses = []
 
87
  while self.message_queue:
88
+ msg = self.message_queue.pop(0)
89
+ resp = self.handle_message(msg)
90
+ responses.append(resp)
91
+ return responses
 
 
 
 
92
 
93
  def get_message_history(self):
 
94
  return self.message_history
95
 
96
+ def handle_message(self, message):
97
+ raise NotImplementedError("Override in subclass")
98
 
99
+ # ============== Compute Agent ==============
100
 
101
  class ComputeAgent(MCPAgent):
 
 
102
  def __init__(self, name="ComputeAgent"):
103
+ super().__init__(name, "Loads and computes on datasets")
104
  self.dataframe = None
105
 
106
+ # Tools
107
+ self.register_tool(MCPTool("load_dataset", "Load a dataset from URL", self._load_dataset))
108
+ self.register_tool(MCPTool("compute_statistics", "Compute basic statistics", self._compute_statistics))
109
+ self.register_tool(MCPTool("compute_correlation", "Compute correlation matrix", self._compute_correlation))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  def _load_dataset(self, params):
112
+ url = params.get("url", "").strip()
113
+ # default cereal dataset
114
+ if not url or url.lower() == "default":
115
+ url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/cereal.csv"
116
  try:
117
+ # fetch via pandas
118
+ self.dataframe = pd.read_csv(url)
119
+ return {
 
 
 
 
 
 
 
 
120
  "status": "success",
121
+ "rows": self.dataframe.shape[0],
122
  "columns": list(self.dataframe.columns),
123
  "preview": self.dataframe.head(5).to_dict(orient="records")
124
  }
 
 
 
125
  except Exception as e:
126
+ logger.exception("Error loading dataset")
127
  return {"status": "error", "message": str(e)}
128
 
129
  def _compute_statistics(self, params):
 
130
  if self.dataframe is None:
131
  return {"status": "error", "message": "No dataset loaded"}
 
132
  try:
133
+ cols = params.get("columns", list(self.dataframe.select_dtypes(include=[np.number]).columns))
134
+ stats = self.dataframe[cols].describe().to_dict()
135
+ return {"status": "success", "statistics": stats}
 
 
 
 
 
 
 
 
136
  except Exception as e:
137
+ logger.exception("Error computing statistics")
138
  return {"status": "error", "message": str(e)}
139
 
140
  def _compute_correlation(self, params):
 
141
  if self.dataframe is None:
142
  return {"status": "error", "message": "No dataset loaded"}
 
143
  try:
144
+ cols = params.get("columns", list(self.dataframe.select_dtypes(include=[np.number]).columns))
145
+ corr = self.dataframe[cols].corr().to_dict()
146
+ return {"status": "success", "correlation_matrix": corr}
 
 
 
 
 
 
 
147
  except Exception as e:
148
+ logger.exception("Error computing correlation")
149
  return {"status": "error", "message": str(e)}
150
 
151
  def handle_message(self, message):
152
+ mtype = message.message_type
153
+ content = message.content
154
+ if mtype == "request_data_load":
155
+ result = self._load_dataset(content)
156
  return self.send_message(message.sender, "data_load_result", result)
157
+ elif mtype == "request_statistics":
158
+ result = self._compute_statistics(content)
 
159
  return self.send_message(message.sender, "statistics_result", result)
160
+ elif mtype == "request_correlation":
161
+ result = self._compute_correlation(content)
 
162
  return self.send_message(message.sender, "correlation_result", result)
 
163
  else:
164
+ return {"status": "error", "message": f"Unknown message type {mtype}"}
 
165
 
166
+ # ============== Interpret Agent ==============
167
 
168
  class InterpretAgent(MCPAgent):
 
 
169
  def __init__(self, name="InterpretAgent"):
170
+ super().__init__(name, "Interprets and reports on results")
171
  self.dataset_info = None
172
  self.statistics = None
173
+ self.correlation = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
+ self.register_tool(MCPTool("interpret_statistics", "", self._interpret_statistics))
176
+ self.register_tool(MCPTool("interpret_correlation", "", self._interpret_correlation))
177
+ self.register_tool(MCPTool("generate_report", "", self._generate_report))
178
 
179
+ def _interpret_statistics(self, params):
180
+ stats = self.statistics.get("statistics", {})
181
+ insights = []
182
+ for col, vals in stats.items():
183
+ if "mean" in vals:
184
+ insights.append(f"{col} avg = {vals['mean']:.2f}")
185
+ if "min" in vals and "max" in vals:
186
+ insights.append(f"{col} ranges {vals['min']:.2f}–{vals['max']:.2f}")
187
+ return {"status": "success", "insights": insights[:3], "summary": "Stats interpreted"}
188
 
189
  def _interpret_correlation(self, params):
190
+ return {"status": "success", "insights": ["Correlation matrix computed"], "summary": ""}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  def _generate_report(self, params):
193
+ sections = []
194
+ if self.dataset_info:
195
+ sections.append({
196
+ "title": "Overview",
197
+ "content": f"{self.dataset_info['rows']} rows × {len(self.dataset_info['columns'])} cols"
 
 
 
 
 
 
 
 
 
 
198
  })
199
+ sections.append({"title": "Conclusion", "content": "Analysis complete."})
200
+ return {
201
+ "status": "success",
202
+ "report": {
203
+ "title": params.get("report_title", "Report"),
204
+ "sections": sections
 
205
  }
206
+ }
 
 
207
 
208
  def handle_message(self, message):
209
+ mtype = message.message_type
210
+ content = message.content
211
+ if mtype == "data_load_result":
212
+ self.dataset_info = content
213
+ return self.send_message(message.sender, "ack", {"status": "loaded"})
214
+ elif mtype == "statistics_result":
215
+ self.statistics = content
216
+ interp = self._interpret_statistics({})
217
+ return self.send_message(message.sender, "statistics_interpretation", interp)
218
+ elif mtype == "correlation_result":
219
+ self.correlation = content
220
+ interp = self._interpret_correlation({})
221
+ return self.send_message(message.sender, "correlation_interpretation", interp)
222
+ elif mtype == "request_report":
223
+ report = self._generate_report(content)
 
 
224
  return self.send_message(message.sender, "report_result", report)
 
225
  else:
226
+ return {"status": "error", "message": f"Unknown message type {mtype}"}
227
 
228
+ # ============== Main Workflow ==============
 
229
 
230
  class DataAnalystDuo:
 
 
231
  def __init__(self):
232
  self.compute_agent = ComputeAgent()
233
  self.interpret_agent = InterpretAgent()
 
 
234
  self.compute_agent.connect(self.interpret_agent)
235
  self.interpret_agent.connect(self.compute_agent)
236
 
237
  def run_analysis(self, dataset_url="default"):
238
+ # 1. Load
 
 
239
  self.interpret_agent.send_message("ComputeAgent", "request_data_load", {"url": dataset_url})
240
+ self.compute_agent.process_messages(); self.interpret_agent.process_messages()
241
+ # 2. Stats
242
+ self.interpret_agent.send_message("ComputeAgent", "request_statistics", {})
243
+ self.compute_agent.process_messages(); self.interpret_agent.process_messages()
244
+ # 3. Corr
245
+ self.interpret_agent.send_message("ComputeAgent", "request_correlation", {})
246
+ self.compute_agent.process_messages(); self.interpret_agent.process_messages()
247
+ # 4. Report
248
+ self.compute_agent.send_message("InterpretAgent", "request_report", {"report_title": "Analysis Report"})
249
+ self.interpret_agent.process_messages(); self.compute_agent.process_messages()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
+ return {
252
+ "compute": self.compute_agent.get_message_history(),
253
+ "interpret": self.interpret_agent.get_message_history()
254
+ }
255
 
256
  # ============== Gradio Interface ==============
257
 
258
+ def format_json(data):
259
+ return json.dumps(data, indent=2) if isinstance(data, (dict, list)) else str(data)
 
 
 
260
 
261
  def run_analysis(dataset_url):
262
+ duo = DataAnalystDuo()
263
+ histories = duo.run_analysis(dataset_url.strip())
264
+
265
+ all_msgs = []
266
+ for side in ["compute", "interpret"]:
267
+ for entry in histories[side]:
268
+ msg = entry["message"]
269
+ line = (f"[{msg['timestamp']}] {msg['sender']} "
270
+ f"{entry['type'].upper()} {msg['message_type']}\n"
271
+ f"{format_json(msg['content'])}\n\n" + "-"*60 + "\n")
272
+ all_msgs.append((msg['timestamp'], line))
273
+ all_msgs.sort(key=lambda x: x[0])
274
+ return "\n".join(line for _, line in all_msgs)
275
+
276
+ with gr.Blocks(title="Data Analyst Duo MCP") as app:
277
+ gr.Markdown("## Data Analyst Duo Load any CSV URL")
278
+ input_box = gr.Textbox(
279
+ label="Dataset URL",
280
+ placeholder="e.g. https://raw.githubusercontent.com/.../diamonds.csv"
281
+ )
282
+ run_btn = gr.Button("Run")
283
+ output_box = gr.Textbox(label="MCP Flow", lines=25)
284
+ run_btn.click(fn=run_analysis, inputs=input_box, outputs=output_box)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
 
286
  if __name__ == "__main__":
287
+ app.launch()