File size: 17,717 Bytes
64ced8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
"""Knowledge Graph Visualization Module for Track 2 Implementation.

This module provides interactive network visualization capabilities using Plotly and NetworkX
for the KGraph-MCP platform, implementing simplified visualization as part of Track 2
hackathon submission.
"""

import logging
from typing import Any

import networkx as nx
import numpy as np
import plotly.graph_objects as go
from plotly.graph_objects import Figure

from .ontology import MCPPrompt, MCPTool, PlannedStep

logger = logging.getLogger(__name__)


class KGVisualizer:
    """Professional knowledge graph visualizer for Track 2 submission.
    
    Provides interactive network visualization using Plotly and NetworkX,
    designed to showcase knowledge graph relationships and planning workflows.
    """

    def __init__(self):
        """Initialize the visualizer with professional color schemes."""
        # Professional color palette - more sophisticated and accessible
        self.colors = {
            # Primary brand colors - more sophisticated blues and teals
            "primary": "#1e40af",      # Deep professional blue
            "primary_light": "#3b82f6", # Accessible light blue
            "accent": "#0ea5e9",       # Professional cyan
            "accent_light": "#06b6d4",  # Lighter teal

            # Node type colors - distinct but harmonious
            "tool": "#059669",         # Professional emerald green
            "prompt": "#7c3aed",       # Rich purple
            "step": "#dc2626",         # Professional red
            "query": "#f59e0b",        # Warm amber

            # UI colors - neutral and professional
            "background": "#f8fafc",   # Light neutral background
            "surface": "#ffffff",      # Pure white surface
            "border": "#e2e8f0",       # Subtle border gray
            "text_primary": "#1e293b", # Dark professional text
            "text_secondary": "#64748b", # Medium gray text

            # Status colors - accessible and clear
            "success": "#10b981",      # Success green
            "warning": "#f59e0b",      # Warning amber
            "error": "#ef4444",        # Error red
            "info": "#3b82f6",         # Info blue

            # Interactive states
            "hover": "#fbbf24",        # Gold hover state
            "selected": "#ec4899",     # Pink selection state
            "disabled": "#9ca3af",     # Gray disabled state
        }

        # Layout configuration for professional appearance
        self.layout_config = {
            "showlegend": True,
            "hovermode": "closest",
            "margin": dict(b=20, l=5, r=5, t=40),
            "annotations": [
                dict(
                    text="KGraph-MCP Knowledge Network - Track 2 Visualization",
                    showarrow=False,
                    xref="paper", yref="paper",
                    x=0.005, y=-0.002,
                    xanchor="left", yanchor="bottom",
                    font=dict(color=self.colors["text_secondary"], size=12)
                )
            ],
            "xaxis": dict(showgrid=False, zeroline=False, showticklabels=False),
            "yaxis": dict(showgrid=False, zeroline=False, showticklabels=False),
            "plot_bgcolor": self.colors["background"],
            "paper_bgcolor": self.colors["surface"],
        }

    def create_plan_visualization(self, planned_steps: list[PlannedStep],
                                query: str = "") -> Figure:
        """Create interactive visualization of planned steps.
        
        Args:
            planned_steps: List of PlannedStep objects to visualize
            query: Original user query for context
            
        Returns:
            Plotly Figure with interactive network visualization
        """
        if not planned_steps:
            return self._create_empty_visualization("No planned steps to visualize")

        try:
            # Create NetworkX graph
            G = nx.Graph()

            # Add query node as central hub
            query_text = query[:50] + "..." if len(query) > 50 else query
            G.add_node("query",
                      type="query",
                      label=f"Query: {query_text}",
                      size=20,
                      color=self.colors["query"])

            # Add nodes and edges for each planned step
            for i, step in enumerate(planned_steps):
                step_id = f"step_{i}"
                tool_id = f"tool_{step.tool.tool_id}"
                prompt_id = f"prompt_{step.prompt.prompt_id}"

                # Add step node
                relevance_text = f" (Score: {step.relevance_score:.2f})" if step.relevance_score else ""
                G.add_node(step_id,
                          type="step",
                          label=f"Step {i+1}{relevance_text}",
                          size=15,
                          color=self.colors["step"],
                          relevance=step.relevance_score or 0.0)

                # Add tool node
                tool_label = f"πŸ”§ {step.tool.name}"
                G.add_node(tool_id,
                          type="tool",
                          label=tool_label,
                          description=step.tool.description,
                          tags=", ".join(step.tool.tags) if step.tool.tags else "No tags",
                          size=12,
                          color=self.colors["tool"])

                # Add prompt node
                prompt_label = f"πŸ“ {step.prompt.name}"
                G.add_node(prompt_id,
                          type="prompt",
                          label=prompt_label,
                          description=step.prompt.description,
                          difficulty=step.prompt.difficulty_level,
                          size=10,
                          color=self.colors["prompt"])

                # Add edges
                G.add_edge("query", step_id, weight=2.0)
                G.add_edge(step_id, tool_id, weight=1.5)
                G.add_edge(step_id, prompt_id, weight=1.5)
                G.add_edge(tool_id, prompt_id, weight=1.0)

            # Generate layout using spring layout for better aesthetics
            pos = nx.spring_layout(G, k=3, iterations=50, seed=42)

            # Create Plotly traces
            traces = self._create_network_traces(G, pos)

            # Create figure
            fig = go.Figure(data=traces)
            fig.update_layout(
                **self.layout_config,
                title=dict(
                    text="🧠 KGraph-MCP Planning Network",
                    x=0.5,
                    font=dict(size=20, color=self.colors["text_primary"])
                )
            )

            return fig

        except Exception as e:
            logger.error(f"Error creating plan visualization: {e}")
            return self._create_error_visualization(f"Visualization error: {e!s}")

    def create_tool_ecosystem_visualization(self, tools: list[MCPTool],
                                          prompts: list[MCPPrompt]) -> Figure:
        """Create ecosystem view of all tools and prompts.
        
        Args:
            tools: List of available tools
            prompts: List of available prompts
            
        Returns:
            Plotly Figure with ecosystem visualization
        """
        try:
            G = nx.Graph()

            # Add tool nodes
            for tool in tools:
                tool_id = f"tool_{tool.tool_id}"
                G.add_node(tool_id,
                          type="tool",
                          label=f"πŸ”§ {tool.name}",
                          description=tool.description,
                          tags=", ".join(tool.tags) if tool.tags else "No tags",
                          size=15,
                          color=self.colors["tool"])

            # Add prompt nodes and connect to tools
            for prompt in prompts:
                prompt_id = f"prompt_{prompt.prompt_id}"
                tool_id = f"tool_{prompt.target_tool_id}"

                G.add_node(prompt_id,
                          type="prompt",
                          label=f"πŸ“ {prompt.name}",
                          description=prompt.description,
                          difficulty=prompt.difficulty_level,
                          size=10,
                          color=self.colors["prompt"])

                # Connect prompt to its target tool
                if tool_id in G.nodes():
                    G.add_edge(tool_id, prompt_id, weight=1.0)

            # Group nodes by tags for better layout
            pos = self._create_clustered_layout(G, tools)

            # Create traces
            traces = self._create_network_traces(G, pos)

            # Create figure
            fig = go.Figure(data=traces)
            fig.update_layout(
                **self.layout_config,
                title=dict(
                    text="🌐 KGraph-MCP Tool Ecosystem",
                    x=0.5,
                    font=dict(size=20, color=self.colors["text_primary"])
                )
            )

            return fig

        except Exception as e:
            logger.error(f"Error creating ecosystem visualization: {e}")
            return self._create_error_visualization(f"Ecosystem visualization error: {e!s}")

    def _create_network_traces(self, G: nx.Graph, pos: dict) -> list[go.Scatter]:
        """Create Plotly traces for network visualization."""
        traces = []

        # Create edge traces
        edge_x, edge_y = [], []
        for edge in G.edges():
            x0, y0 = pos[edge[0]]
            x1, y1 = pos[edge[1]]
            edge_x.extend([x0, x1, None])
            edge_y.extend([y0, y1, None])

        edge_trace = go.Scatter(
            x=edge_x, y=edge_y,
            line=dict(width=2, color=self.colors["border"]),
            hoverinfo="none",
            mode="lines",
            name="Connections",
            showlegend=False
        )
        traces.append(edge_trace)

        # Create node traces by type
        node_types = set(G.nodes[node].get("type", "unknown") for node in G.nodes())

        for node_type in node_types:
            nodes_of_type = [node for node in G.nodes()
                           if G.nodes[node].get("type") == node_type]

            if not nodes_of_type:
                continue

            node_x = [pos[node][0] for node in nodes_of_type]
            node_y = [pos[node][1] for node in nodes_of_type]

            # Get node attributes
            node_colors = [G.nodes[node].get("color", self.colors["disabled"])
                          for node in nodes_of_type]
            node_sizes = [G.nodes[node].get("size", 10) for node in nodes_of_type]
            node_labels = [G.nodes[node].get("label", node) for node in nodes_of_type]

            # Create hover text
            hover_texts = []
            for node in nodes_of_type:
                node_data = G.nodes[node]
                hover_text = f"<b>{node_data.get('label', node)}</b><br>"

                if "description" in node_data:
                    hover_text += f"Description: {node_data['description']}<br>"
                if "tags" in node_data:
                    hover_text += f"Tags: {node_data['tags']}<br>"
                if "difficulty" in node_data:
                    hover_text += f"Difficulty: {node_data['difficulty']}<br>"
                if "relevance" in node_data:
                    hover_text += f"Relevance: {node_data['relevance']:.2f}<br>"

                hover_texts.append(hover_text)

            # Create node trace
            node_trace = go.Scatter(
                x=node_x, y=node_y,
                mode="markers+text",
                text=node_labels,
                textposition="middle center",
                textfont=dict(size=10, color=self.colors["surface"]),
                hovertemplate="%{hovertext}<extra></extra>",
                hovertext=hover_texts,
                marker=dict(
                    size=node_sizes,
                    color=node_colors,
                    line=dict(width=2, color=self.colors["surface"]),
                    sizemode="diameter"
                ),
                name=node_type.title(),
                showlegend=True
            )
            traces.append(node_trace)

        return traces

    def _create_clustered_layout(self, G: nx.Graph, tools: list[MCPTool]) -> dict:
        """Create clustered layout based on tool tags."""
        # Group tools by their primary tag
        tag_groups = {}
        for tool in tools:
            primary_tag = tool.tags[0] if tool.tags else "general"
            if primary_tag not in tag_groups:
                tag_groups[primary_tag] = []
            tag_groups[primary_tag].append(f"tool_{tool.tool_id}")

        # Create positions with clustering
        pos = {}
        angle_step = 2 * 3.14159 / len(tag_groups) if tag_groups else 1

        for i, (tag, tool_ids) in enumerate(tag_groups.items()):
            center_x = 3 * np.cos(i * angle_step)
            center_y = 3 * np.sin(i * angle_step)

            # Layout tools in this group
            for j, tool_id in enumerate(tool_ids):
                offset_angle = j * 0.5
                offset_radius = 0.8
                pos[tool_id] = (
                    center_x + offset_radius * np.cos(offset_angle),
                    center_y + offset_radius * np.sin(offset_angle)
                )

        # Position prompts near their tools
        for node in G.nodes():
            if node.startswith("prompt_"):
                # Find connected tools
                connected_tools = [neighbor for neighbor in G.neighbors(node)
                                 if neighbor.startswith("tool_")]
                if connected_tools:
                    tool_pos = pos[connected_tools[0]]
                    # Offset prompt position slightly
                    pos[node] = (tool_pos[0] + 0.3, tool_pos[1] + 0.3)
                else:
                    pos[node] = (0, 0)  # Default position

        return pos

    def _create_empty_visualization(self, message: str) -> Figure:
        """Create empty state visualization."""
        fig = go.Figure()
        fig.add_annotation(
            text=f"πŸ“Š {message}",
            xref="paper", yref="paper",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=16, color=self.colors["text_secondary"])
        )
        fig.update_layout(**self.layout_config)
        return fig

    def _create_error_visualization(self, error_msg: str) -> Figure:
        """Create error state visualization."""
        fig = go.Figure()
        fig.add_annotation(
            text=f"⚠️ {error_msg}",
            xref="paper", yref="paper",
            x=0.5, y=0.5,
            showarrow=False,
            font=dict(size=16, color=self.colors["error"])
        )
        fig.update_layout(**self.layout_config)
        return fig

    def create_performance_metrics_chart(self, metrics_data: dict[str, Any]) -> Figure:
        """Create performance metrics visualization for demo purposes."""
        try:
            # Sample metrics for demonstration
            categories = ["Response Time", "Accuracy", "Coverage", "Relevance", "User Satisfaction"]
            values = [95, 88, 92, 90, 94]  # Sample scores

            # Create radar chart
            fig = go.Figure(data=go.Scatterpolar(
                r=values,
                theta=categories,
                fill="toself",
                fillcolor=f"rgba({self._hex_to_rgb(self.colors['primary'])}, 0.3)",
                line=dict(color=self.colors["primary"], width=3),
                marker=dict(size=8, color=self.colors["accent"]),
                name="KGraph-MCP Performance"
            ))

            fig.update_layout(
                polar=dict(
                    radialaxis=dict(
                        visible=True,
                        range=[0, 100],
                        tickmode="linear",
                        tick0=0,
                        dtick=20,
                        gridcolor=self.colors["border"]
                    ),
                    angularaxis=dict(
                        gridcolor=self.colors["border"]
                    )
                ),
                showlegend=True,
                title=dict(
                    text="πŸ“ˆ Platform Performance Metrics",
                    x=0.5,
                    font=dict(size=18, color=self.colors["text_primary"])
                ),
                plot_bgcolor=self.colors["background"],
                paper_bgcolor=self.colors["surface"]
            )

            return fig

        except Exception as e:
            logger.error(f"Error creating performance chart: {e}")
            return self._create_error_visualization(f"Performance chart error: {e!s}")

    def _hex_to_rgb(self, hex_color: str) -> str:
        """Convert hex color to RGB string."""
        hex_color = hex_color.lstrip("#")
        return f"{int(hex_color[0:2], 16)}, {int(hex_color[2:4], 16)}, {int(hex_color[4:6], 16)}"


# Convenience function for easy integration
def create_plan_visualization(planned_steps: list[PlannedStep], query: str = "") -> Figure:
    """Convenience function to create plan visualization."""
    visualizer = KGVisualizer()
    return visualizer.create_plan_visualization(planned_steps, query)


def create_ecosystem_visualization(tools: list[MCPTool], prompts: list[MCPPrompt]) -> Figure:
    """Convenience function to create ecosystem visualization."""
    visualizer = KGVisualizer()
    return visualizer.create_tool_ecosystem_visualization(tools, prompts)