tooluniverse 0.1.4__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/__init__.py +340 -4
- tooluniverse/admetai_tool.py +84 -0
- tooluniverse/agentic_tool.py +563 -0
- tooluniverse/alphafold_tool.py +96 -0
- tooluniverse/base_tool.py +129 -6
- tooluniverse/boltz_tool.py +207 -0
- tooluniverse/chem_tool.py +192 -0
- tooluniverse/compose_scripts/__init__.py +1 -0
- tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
- tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
- tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
- tooluniverse/compose_scripts/literature_tool.py +34 -0
- tooluniverse/compose_scripts/output_summarizer.py +279 -0
- tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
- tooluniverse/compose_scripts/tool_discover.py +705 -0
- tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
- tooluniverse/compose_tool.py +371 -0
- tooluniverse/ctg_tool.py +1002 -0
- tooluniverse/custom_tool.py +81 -0
- tooluniverse/dailymed_tool.py +108 -0
- tooluniverse/data/admetai_tools.json +155 -0
- tooluniverse/data/agentic_tools.json +1156 -0
- tooluniverse/data/alphafold_tools.json +87 -0
- tooluniverse/data/boltz_tools.json +9 -0
- tooluniverse/data/chembl_tools.json +16 -0
- tooluniverse/data/clait_tools.json +108 -0
- tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
- tooluniverse/data/compose_tools.json +202 -0
- tooluniverse/data/dailymed_tools.json +70 -0
- tooluniverse/data/dataset_tools.json +646 -0
- tooluniverse/data/disease_target_score_tools.json +712 -0
- tooluniverse/data/efo_tools.json +17 -0
- tooluniverse/data/embedding_tools.json +319 -0
- tooluniverse/data/enrichr_tools.json +31 -0
- tooluniverse/data/europe_pmc_tools.json +22 -0
- tooluniverse/data/expert_feedback_tools.json +10 -0
- tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
- tooluniverse/data/fda_drug_labeling_tools.json +544 -168
- tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
- tooluniverse/data/finder_tools.json +209 -0
- tooluniverse/data/gene_ontology_tools.json +113 -0
- tooluniverse/data/gwas_tools.json +1082 -0
- tooluniverse/data/hpa_tools.json +333 -0
- tooluniverse/data/humanbase_tools.json +47 -0
- tooluniverse/data/idmap_tools.json +74 -0
- tooluniverse/data/mcp_client_tools_example.json +113 -0
- tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
- tooluniverse/data/medlineplus_tools.json +141 -0
- tooluniverse/data/monarch_tools.json +1 -1
- tooluniverse/data/openalex_tools.json +36 -0
- tooluniverse/data/opentarget_tools.json +82 -58
- tooluniverse/data/output_summarization_tools.json +101 -0
- tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
- tooluniverse/data/packages/categorized_tools.txt +206 -0
- tooluniverse/data/packages/cheminformatics_tools.json +347 -0
- tooluniverse/data/packages/earth_sciences_tools.json +74 -0
- tooluniverse/data/packages/genomics_tools.json +776 -0
- tooluniverse/data/packages/image_processing_tools.json +38 -0
- tooluniverse/data/packages/machine_learning_tools.json +789 -0
- tooluniverse/data/packages/neuroscience_tools.json +62 -0
- tooluniverse/data/packages/original_tools.txt +0 -0
- tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
- tooluniverse/data/packages/scientific_computing_tools.json +560 -0
- tooluniverse/data/packages/single_cell_tools.json +453 -0
- tooluniverse/data/packages/software_tools.json +4954 -0
- tooluniverse/data/packages/structural_biology_tools.json +396 -0
- tooluniverse/data/packages/visualization_tools.json +399 -0
- tooluniverse/data/pubchem_tools.json +215 -0
- tooluniverse/data/pubtator_tools.json +68 -0
- tooluniverse/data/rcsb_pdb_tools.json +1332 -0
- tooluniverse/data/reactome_tools.json +19 -0
- tooluniverse/data/semantic_scholar_tools.json +26 -0
- tooluniverse/data/special_tools.json +2 -25
- tooluniverse/data/tool_composition_tools.json +88 -0
- tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
- tooluniverse/data/txagent_client_tools.json +9 -0
- tooluniverse/data/uniprot_tools.json +211 -0
- tooluniverse/data/url_fetch_tools.json +94 -0
- tooluniverse/data/uspto_downloader_tools.json +9 -0
- tooluniverse/data/uspto_tools.json +811 -0
- tooluniverse/data/xml_tools.json +3275 -0
- tooluniverse/dataset_tool.py +296 -0
- tooluniverse/default_config.py +165 -0
- tooluniverse/efo_tool.py +42 -0
- tooluniverse/embedding_database.py +630 -0
- tooluniverse/embedding_sync.py +396 -0
- tooluniverse/enrichr_tool.py +266 -0
- tooluniverse/europe_pmc_tool.py +52 -0
- tooluniverse/execute_function.py +1775 -95
- tooluniverse/extended_hooks.py +444 -0
- tooluniverse/gene_ontology_tool.py +194 -0
- tooluniverse/graphql_tool.py +158 -36
- tooluniverse/gwas_tool.py +358 -0
- tooluniverse/hpa_tool.py +1645 -0
- tooluniverse/humanbase_tool.py +389 -0
- tooluniverse/logging_config.py +254 -0
- tooluniverse/mcp_client_tool.py +764 -0
- tooluniverse/mcp_integration.py +413 -0
- tooluniverse/mcp_tool_registry.py +925 -0
- tooluniverse/medlineplus_tool.py +337 -0
- tooluniverse/openalex_tool.py +228 -0
- tooluniverse/openfda_adv_tool.py +283 -0
- tooluniverse/openfda_tool.py +393 -160
- tooluniverse/output_hook.py +1122 -0
- tooluniverse/package_tool.py +195 -0
- tooluniverse/pubchem_tool.py +158 -0
- tooluniverse/pubtator_tool.py +168 -0
- tooluniverse/rcsb_pdb_tool.py +38 -0
- tooluniverse/reactome_tool.py +108 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
- tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
- tooluniverse/remote/expert_feedback/simple_test.py +23 -0
- tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
- tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
- tooluniverse/remote/expert_feedback_mcp/human_expert_mcp_server.py +1611 -0
- tooluniverse/remote/expert_feedback_mcp/simple_test.py +34 -0
- tooluniverse/remote/expert_feedback_mcp/start_web_interface.py +91 -0
- tooluniverse/remote/immune_compass/compass_tool.py +327 -0
- tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
- tooluniverse/remote_tool.py +99 -0
- tooluniverse/restful_tool.py +53 -30
- tooluniverse/scripts/generate_tool_graph.py +408 -0
- tooluniverse/scripts/visualize_tool_graph.py +829 -0
- tooluniverse/semantic_scholar_tool.py +62 -0
- tooluniverse/smcp.py +2452 -0
- tooluniverse/smcp_server.py +975 -0
- tooluniverse/test/mcp_server_test.py +0 -0
- tooluniverse/test/test_admetai_tool.py +370 -0
- tooluniverse/test/test_agentic_tool.py +129 -0
- tooluniverse/test/test_alphafold_tool.py +71 -0
- tooluniverse/test/test_chem_tool.py +37 -0
- tooluniverse/test/test_compose_lieraturereview.py +63 -0
- tooluniverse/test/test_compose_tool.py +448 -0
- tooluniverse/test/test_dailymed.py +69 -0
- tooluniverse/test/test_dataset_tool.py +200 -0
- tooluniverse/test/test_disease_target_score.py +56 -0
- tooluniverse/test/test_drugbank_filter_examples.py +179 -0
- tooluniverse/test/test_efo.py +31 -0
- tooluniverse/test/test_enrichr_tool.py +21 -0
- tooluniverse/test/test_europe_pmc_tool.py +20 -0
- tooluniverse/test/test_fda_adv.py +95 -0
- tooluniverse/test/test_fda_drug_labeling.py +91 -0
- tooluniverse/test/test_gene_ontology_tools.py +66 -0
- tooluniverse/test/test_gwas_tool.py +139 -0
- tooluniverse/test/test_hpa.py +625 -0
- tooluniverse/test/test_humanbase_tool.py +20 -0
- tooluniverse/test/test_idmap_tools.py +61 -0
- tooluniverse/test/test_mcp_server.py +211 -0
- tooluniverse/test/test_mcp_tool.py +247 -0
- tooluniverse/test/test_medlineplus.py +220 -0
- tooluniverse/test/test_openalex_tool.py +32 -0
- tooluniverse/test/test_opentargets.py +28 -0
- tooluniverse/test/test_pubchem_tool.py +116 -0
- tooluniverse/test/test_pubtator_tool.py +37 -0
- tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
- tooluniverse/test/test_reactome.py +54 -0
- tooluniverse/test/test_semantic_scholar_tool.py +24 -0
- tooluniverse/test/test_software_tools.py +147 -0
- tooluniverse/test/test_tool_description_optimizer.py +49 -0
- tooluniverse/test/test_tool_finder.py +26 -0
- tooluniverse/test/test_tool_finder_llm.py +252 -0
- tooluniverse/test/test_tools_find.py +195 -0
- tooluniverse/test/test_uniprot_tools.py +74 -0
- tooluniverse/test/test_uspto_tool.py +72 -0
- tooluniverse/test/test_xml_tool.py +113 -0
- tooluniverse/tool_finder_embedding.py +267 -0
- tooluniverse/tool_finder_keyword.py +693 -0
- tooluniverse/tool_finder_llm.py +699 -0
- tooluniverse/tool_graph_web_ui.py +955 -0
- tooluniverse/tool_registry.py +416 -0
- tooluniverse/uniprot_tool.py +155 -0
- tooluniverse/url_tool.py +253 -0
- tooluniverse/uspto_tool.py +240 -0
- tooluniverse/utils.py +369 -41
- tooluniverse/xml_tool.py +369 -0
- tooluniverse-1.0.0.dist-info/METADATA +377 -0
- tooluniverse-1.0.0.dist-info/RECORD +186 -0
- {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/WHEEL +1 -1
- tooluniverse-1.0.0.dist-info/entry_points.txt +9 -0
- tooluniverse-0.1.4.dist-info/METADATA +0 -141
- tooluniverse-0.1.4.dist-info/RECORD +0 -18
- {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-0.1.4.dist-info → tooluniverse-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tool Graph Visualizer Script
|
|
4
|
+
|
|
5
|
+
This script provides interactive visualization of tool compatibility graphs.
|
|
6
|
+
It can launch web-based interfaces for exploring tool relationships.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python visualize_tool_graph.py [graph_file] [options]
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
# Launch web viewer with default graph
|
|
13
|
+
python visualize_tool_graph.py
|
|
14
|
+
|
|
15
|
+
# Visualize specific graph file
|
|
16
|
+
python visualize_tool_graph.py ./my_graph.json
|
|
17
|
+
|
|
18
|
+
# Launch with specific port and open browser
|
|
19
|
+
python visualize_tool_graph.py --port 5001 --open-browser
|
|
20
|
+
|
|
21
|
+
# Generate static visualization image
|
|
22
|
+
python visualize_tool_graph.py --export-image graph.png --format png
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import sys
|
|
28
|
+
import webbrowser
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
|
|
32
|
+
# Add parent directories to path for imports
|
|
33
|
+
script_dir = Path(__file__).parent
|
|
34
|
+
tooluniverse_dir = script_dir.parent
|
|
35
|
+
src_dir = tooluniverse_dir.parent
|
|
36
|
+
sys.path.insert(0, str(src_dir))
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from flask import (
|
|
40
|
+
Flask,
|
|
41
|
+
render_template_string,
|
|
42
|
+
jsonify,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
FLASK_AVAILABLE = True
|
|
46
|
+
except ImportError:
|
|
47
|
+
FLASK_AVAILABLE = False
|
|
48
|
+
FLASK_IMPORT_ERROR = """
|
|
49
|
+
❌ Flask is required for web visualization but not installed.
|
|
50
|
+
|
|
51
|
+
To install graph visualization dependencies:
|
|
52
|
+
pip install tooluniverse[graph]
|
|
53
|
+
|
|
54
|
+
Or install all optional dependencies:
|
|
55
|
+
pip install tooluniverse[all]
|
|
56
|
+
|
|
57
|
+
Alternatively, use --export-image to create a static visualization.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_arguments():
|
|
62
|
+
"""Parse command line arguments."""
|
|
63
|
+
|
|
64
|
+
parser = argparse.ArgumentParser(
|
|
65
|
+
description="Visualize tool compatibility graphs",
|
|
66
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
67
|
+
epilog="""
|
|
68
|
+
Examples:
|
|
69
|
+
%(prog)s # Use default graph
|
|
70
|
+
%(prog)s graph.json # Visualize specific graph
|
|
71
|
+
%(prog)s --port 5001 --open-browser # Custom port and auto-open
|
|
72
|
+
%(prog)s --export-image graph.png # Export static image
|
|
73
|
+
""",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"graph_file", nargs="?", help="Path to graph JSON file (default: auto-detect)"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Server options
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--port",
|
|
83
|
+
"-p",
|
|
84
|
+
type=int,
|
|
85
|
+
default=5000,
|
|
86
|
+
help="Port for web server (default: 5000)",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--host",
|
|
91
|
+
type=str,
|
|
92
|
+
default="127.0.0.1",
|
|
93
|
+
help="Host for web server (default: 127.0.0.1)",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--open-browser", "-o", action="store_true", help="Automatically open browser"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--no-auto-reload",
|
|
102
|
+
action="store_true",
|
|
103
|
+
help="Disable auto-reload on file changes",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Export options
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--export-image",
|
|
109
|
+
type=str,
|
|
110
|
+
help="Export static image to file (requires additional dependencies)",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--format",
|
|
115
|
+
choices=["png", "svg", "pdf", "html"],
|
|
116
|
+
default="png",
|
|
117
|
+
help="Export format (default: png)",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Display options
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--theme",
|
|
123
|
+
choices=["light", "dark", "auto"],
|
|
124
|
+
default="auto",
|
|
125
|
+
help="UI theme (default: auto)",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
parser.add_argument(
|
|
129
|
+
"--layout",
|
|
130
|
+
choices=["force", "hierarchical", "circular", "random"],
|
|
131
|
+
default="force",
|
|
132
|
+
help="Graph layout algorithm (default: force)",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
|
|
136
|
+
|
|
137
|
+
return parser.parse_args()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def find_graph_file(specified_path=None):
|
|
141
|
+
"""Find graph file to visualize."""
|
|
142
|
+
|
|
143
|
+
# List of potential locations to search
|
|
144
|
+
search_paths = []
|
|
145
|
+
|
|
146
|
+
if specified_path:
|
|
147
|
+
search_paths.append(Path(specified_path))
|
|
148
|
+
|
|
149
|
+
# Add current directory and common output locations
|
|
150
|
+
current_dir = Path.cwd()
|
|
151
|
+
search_paths.extend(
|
|
152
|
+
[
|
|
153
|
+
current_dir / "tool_composition_graph.json",
|
|
154
|
+
current_dir / "graph.json",
|
|
155
|
+
current_dir / "tool_graph.json",
|
|
156
|
+
script_dir.parent.parent.parent
|
|
157
|
+
/ "tool_composition_graph.json", # Project root
|
|
158
|
+
script_dir.parent.parent.parent / "graph.json",
|
|
159
|
+
current_dir / "src" / "tooluniverse" / "tool_composition_graph.json",
|
|
160
|
+
]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Find first existing file
|
|
164
|
+
for path in search_paths:
|
|
165
|
+
if path.exists() and path.is_file():
|
|
166
|
+
return path
|
|
167
|
+
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def load_graph_data(graph_file):
|
|
172
|
+
"""Load and validate graph data."""
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
with open(graph_file, "r", encoding="utf-8") as f:
|
|
176
|
+
data = json.load(f)
|
|
177
|
+
|
|
178
|
+
# Validate basic structure
|
|
179
|
+
if not isinstance(data, dict):
|
|
180
|
+
raise ValueError("Graph file must contain a JSON object")
|
|
181
|
+
|
|
182
|
+
if "nodes" not in data or "edges" not in data:
|
|
183
|
+
raise ValueError("Graph file must contain 'nodes' and 'edges' fields")
|
|
184
|
+
|
|
185
|
+
return data
|
|
186
|
+
|
|
187
|
+
except json.JSONDecodeError as e:
|
|
188
|
+
raise ValueError(f"Invalid JSON format: {e}")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
raise ValueError(f"Error loading graph file: {e}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_graph_summary(data):
|
|
194
|
+
"""Get summary information about the graph."""
|
|
195
|
+
|
|
196
|
+
nodes = data.get("nodes", [])
|
|
197
|
+
edges = data.get("edges", [])
|
|
198
|
+
|
|
199
|
+
summary = {
|
|
200
|
+
"node_count": len(nodes),
|
|
201
|
+
"edge_count": len(edges),
|
|
202
|
+
"categories": set(),
|
|
203
|
+
"edge_density": 0.0,
|
|
204
|
+
"avg_compatibility": 0.0,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
# Analyze nodes
|
|
208
|
+
for node in nodes:
|
|
209
|
+
if "category" in node:
|
|
210
|
+
summary["categories"].add(node["category"])
|
|
211
|
+
|
|
212
|
+
summary["category_count"] = len(summary["categories"])
|
|
213
|
+
summary["categories"] = sorted(list(summary["categories"]))
|
|
214
|
+
|
|
215
|
+
# Analyze edges
|
|
216
|
+
if edges and nodes:
|
|
217
|
+
max_edges = len(nodes) * (len(nodes) - 1)
|
|
218
|
+
if max_edges > 0:
|
|
219
|
+
summary["edge_density"] = len(edges) / max_edges
|
|
220
|
+
|
|
221
|
+
# Calculate average compatibility
|
|
222
|
+
scores = [
|
|
223
|
+
edge.get("compatibility_score", 0)
|
|
224
|
+
for edge in edges
|
|
225
|
+
if "compatibility_score" in edge
|
|
226
|
+
]
|
|
227
|
+
if scores:
|
|
228
|
+
summary["avg_compatibility"] = sum(scores) / len(scores)
|
|
229
|
+
|
|
230
|
+
return summary
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def create_web_app(graph_data, theme="auto", layout="force"):
|
|
234
|
+
"""Create Flask web application for graph visualization."""
|
|
235
|
+
|
|
236
|
+
if not FLASK_AVAILABLE:
|
|
237
|
+
raise ImportError(FLASK_IMPORT_ERROR)
|
|
238
|
+
|
|
239
|
+
app = Flask(__name__)
|
|
240
|
+
|
|
241
|
+
# HTML template for the visualization
|
|
242
|
+
HTML_TEMPLATE = """
|
|
243
|
+
<!DOCTYPE html>
|
|
244
|
+
<html lang="en">
|
|
245
|
+
<head>
|
|
246
|
+
<meta charset="UTF-8">
|
|
247
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
248
|
+
<title>ToolUniverse Graph Visualizer</title>
|
|
249
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
250
|
+
<style>
|
|
251
|
+
body {
|
|
252
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
253
|
+
margin: 0;
|
|
254
|
+
padding: 0;
|
|
255
|
+
background: {{ 'white' if theme == 'light' else '#1a1a1a' if theme == 'dark' else 'white' }};
|
|
256
|
+
color: {{ 'black' if theme == 'light' else '#e0e0e0' if theme == 'dark' else 'black' }};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.header {
|
|
260
|
+
background: {{ '#f5f5f5' if theme == 'light' else '#2d2d2d' if theme == 'dark' else '#f5f5f5' }};
|
|
261
|
+
padding: 20px;
|
|
262
|
+
border-bottom: 1px solid {{ '#ddd' if theme == 'light' else '#444' if theme == 'dark' else '#ddd' }};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.header h1 {
|
|
266
|
+
margin: 0;
|
|
267
|
+
font-size: 24px;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.header .summary {
|
|
272
|
+
margin-top: 10px;
|
|
273
|
+
font-size: 14px;
|
|
274
|
+
opacity: 0.8;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.controls {
|
|
278
|
+
padding: 15px 20px;
|
|
279
|
+
background: {{ '#fafafa' if theme == 'light' else '#252525' if theme == 'dark' else '#fafafa' }};
|
|
280
|
+
border-bottom: 1px solid {{ '#eee' if theme == 'light' else '#333' if theme == 'dark' else '#eee' }};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.control-group {
|
|
284
|
+
display: inline-block;
|
|
285
|
+
margin-right: 20px;
|
|
286
|
+
margin-bottom: 10px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.control-group label {
|
|
290
|
+
font-size: 12px;
|
|
291
|
+
font-weight: 500;
|
|
292
|
+
display: block;
|
|
293
|
+
margin-bottom: 5px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.control-group input, .control-group select {
|
|
297
|
+
padding: 5px 8px;
|
|
298
|
+
border: 1px solid {{ '#ccc' if theme == 'light' else '#555' if theme == 'dark' else '#ccc' }};
|
|
299
|
+
border-radius: 4px;
|
|
300
|
+
background: {{ 'white' if theme == 'light' else '#333' if theme == 'dark' else 'white' }};
|
|
301
|
+
color: {{ 'black' if theme == 'light' else 'white' if theme == 'dark' else 'black' }};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.graph-container {
|
|
305
|
+
position: relative;
|
|
306
|
+
height: calc(100vh - 140px);
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.node {
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
stroke-width: 2px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.edge {
|
|
316
|
+
stroke: #999;
|
|
317
|
+
stroke-opacity: 0.6;
|
|
318
|
+
fill: none;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.node-label {
|
|
322
|
+
font-size: 10px;
|
|
323
|
+
text-anchor: middle;
|
|
324
|
+
pointer-events: none;
|
|
325
|
+
fill: {{ 'black' if theme == 'light' else 'white' if theme == 'dark' else 'black' }};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.tooltip {
|
|
329
|
+
position: absolute;
|
|
330
|
+
padding: 10px;
|
|
331
|
+
background: {{ 'rgba(0,0,0,0.8)' if theme == 'light' else 'rgba(255,255,255,0.9)' if theme == 'dark' else 'rgba(0,0,0,0.8)' }};
|
|
332
|
+
color: {{ 'white' if theme == 'light' else 'black' if theme == 'dark' else 'white' }};
|
|
333
|
+
border-radius: 4px;
|
|
334
|
+
font-size: 12px;
|
|
335
|
+
pointer-events: none;
|
|
336
|
+
z-index: 1000;
|
|
337
|
+
max-width: 300px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.legend {
|
|
341
|
+
position: absolute;
|
|
342
|
+
top: 20px;
|
|
343
|
+
right: 20px;
|
|
344
|
+
background: {{ 'rgba(255,255,255,0.95)' if theme == 'light' else 'rgba(0,0,0,0.85)' if theme == 'dark' else 'rgba(255,255,255,0.95)' }};
|
|
345
|
+
padding: 15px;
|
|
346
|
+
border-radius: 8px;
|
|
347
|
+
border: 1px solid {{ '#ddd' if theme == 'light' else '#444' if theme == 'dark' else '#ddd' }};
|
|
348
|
+
font-size: 12px;
|
|
349
|
+
max-height: 300px;
|
|
350
|
+
overflow-y: auto;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.stats {
|
|
354
|
+
position: absolute;
|
|
355
|
+
bottom: 20px;
|
|
356
|
+
left: 20px;
|
|
357
|
+
background: {{ 'rgba(255,255,255,0.95)' if theme == 'light' else 'rgba(0,0,0,0.85)' if theme == 'dark' else 'rgba(255,255,255,0.95)' }};
|
|
358
|
+
padding: 10px;
|
|
359
|
+
border-radius: 4px;
|
|
360
|
+
border: 1px solid {{ '#ddd' if theme == 'light' else '#444' if theme == 'dark' else '#ddd' }};
|
|
361
|
+
font-size: 11px;
|
|
362
|
+
}
|
|
363
|
+
</style>
|
|
364
|
+
</head>
|
|
365
|
+
<body>
|
|
366
|
+
<div class="header">
|
|
367
|
+
<h1>ToolUniverse Graph Visualizer</h1>
|
|
368
|
+
<div class="summary">
|
|
369
|
+
{{ summary.node_count }} tools, {{ summary.edge_count }} connections,
|
|
370
|
+
{{ summary.category_count }} categories
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div class="controls">
|
|
375
|
+
<div class="control-group">
|
|
376
|
+
<label>Search Tools:</label>
|
|
377
|
+
<input type="text" id="searchInput" placeholder="Type tool name...">
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div class="control-group">
|
|
381
|
+
<label>Category Filter:</label>
|
|
382
|
+
<select id="categoryFilter">
|
|
383
|
+
<option value="">All Categories</option>
|
|
384
|
+
{% for category in summary.categories %}
|
|
385
|
+
<option value="{{ category }}">{{ category }}</option>
|
|
386
|
+
{% endfor %}
|
|
387
|
+
</select>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="control-group">
|
|
391
|
+
<label>Min Compatibility:</label>
|
|
392
|
+
<input type="range" id="compatibilitySlider" min="0" max="100" value="0">
|
|
393
|
+
<span id="compatibilityValue">0</span>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<div class="control-group">
|
|
397
|
+
<label>Layout:</label>
|
|
398
|
+
<select id="layoutSelect">
|
|
399
|
+
<option value="force" {{ 'selected' if layout == 'force' else '' }}>Force-Directed</option>
|
|
400
|
+
<option value="circular" {{ 'selected' if layout == 'circular' else '' }}>Circular</option>
|
|
401
|
+
<option value="hierarchical" {{ 'selected' if layout == 'hierarchical' else '' }}>Hierarchical</option>
|
|
402
|
+
</select>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div class="graph-container">
|
|
407
|
+
<svg id="graph"></svg>
|
|
408
|
+
<div class="tooltip" id="tooltip" style="display: none;"></div>
|
|
409
|
+
<div class="legend" id="legend"></div>
|
|
410
|
+
<div class="stats" id="stats"></div>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<script>
|
|
414
|
+
// Graph data from server
|
|
415
|
+
const graphData = {{ graph_data | tojsonfilter }};
|
|
416
|
+
|
|
417
|
+
// D3 visualization code
|
|
418
|
+
const width = window.innerWidth;
|
|
419
|
+
const height = window.innerHeight - 140;
|
|
420
|
+
|
|
421
|
+
const svg = d3.select("#graph")
|
|
422
|
+
.attr("width", width)
|
|
423
|
+
.attr("height", height);
|
|
424
|
+
|
|
425
|
+
const g = svg.append("g");
|
|
426
|
+
|
|
427
|
+
// Zoom behavior
|
|
428
|
+
const zoom = d3.zoom()
|
|
429
|
+
.scaleExtent([0.1, 10])
|
|
430
|
+
.on("zoom", (event) => {
|
|
431
|
+
g.attr("transform", event.transform);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
svg.call(zoom);
|
|
435
|
+
|
|
436
|
+
// Color scale for categories
|
|
437
|
+
const categories = [...new Set(graphData.nodes.map(d => d.category || 'unknown'))];
|
|
438
|
+
const colorScale = d3.scaleOrdinal(d3.schemeCategory10)
|
|
439
|
+
.domain(categories);
|
|
440
|
+
|
|
441
|
+
// Initialize simulation
|
|
442
|
+
let simulation = d3.forceSimulation()
|
|
443
|
+
.force("link", d3.forceLink().id(d => d.id).distance(100))
|
|
444
|
+
.force("charge", d3.forceManyBody().strength(-300))
|
|
445
|
+
.force("center", d3.forceCenter(width / 2, height / 2));
|
|
446
|
+
|
|
447
|
+
// Current data for filtering
|
|
448
|
+
let currentNodes = [...graphData.nodes];
|
|
449
|
+
let currentEdges = [...graphData.edges];
|
|
450
|
+
|
|
451
|
+
function updateVisualization() {
|
|
452
|
+
// Clear existing elements
|
|
453
|
+
g.selectAll("*").remove();
|
|
454
|
+
|
|
455
|
+
// Create edges
|
|
456
|
+
const edge = g.append("g")
|
|
457
|
+
.selectAll("line")
|
|
458
|
+
.data(currentEdges)
|
|
459
|
+
.enter().append("line")
|
|
460
|
+
.attr("class", "edge")
|
|
461
|
+
.attr("stroke-width", d => Math.sqrt(d.compatibility_score || 50) / 5);
|
|
462
|
+
|
|
463
|
+
// Create nodes
|
|
464
|
+
const node = g.append("g")
|
|
465
|
+
.selectAll("circle")
|
|
466
|
+
.data(currentNodes)
|
|
467
|
+
.enter().append("circle")
|
|
468
|
+
.attr("class", "node")
|
|
469
|
+
.attr("r", d => Math.sqrt(d.connections || 1) * 3 + 5)
|
|
470
|
+
.attr("fill", d => colorScale(d.category || 'unknown'))
|
|
471
|
+
.attr("stroke", "#fff")
|
|
472
|
+
.call(d3.drag()
|
|
473
|
+
.on("start", dragstarted)
|
|
474
|
+
.on("drag", dragged)
|
|
475
|
+
.on("end", dragended));
|
|
476
|
+
|
|
477
|
+
// Add labels
|
|
478
|
+
const label = g.append("g")
|
|
479
|
+
.selectAll("text")
|
|
480
|
+
.data(currentNodes)
|
|
481
|
+
.enter().append("text")
|
|
482
|
+
.attr("class", "node-label")
|
|
483
|
+
.text(d => d.name || d.id);
|
|
484
|
+
|
|
485
|
+
// Tooltip interactions
|
|
486
|
+
node.on("mouseover", showTooltip)
|
|
487
|
+
.on("mouseout", hideTooltip);
|
|
488
|
+
|
|
489
|
+
// Update simulation
|
|
490
|
+
simulation.nodes(currentNodes);
|
|
491
|
+
simulation.force("link").links(currentEdges);
|
|
492
|
+
simulation.alpha(1).restart();
|
|
493
|
+
|
|
494
|
+
simulation.on("tick", () => {
|
|
495
|
+
edge
|
|
496
|
+
.attr("x1", d => d.source.x)
|
|
497
|
+
.attr("y1", d => d.source.y)
|
|
498
|
+
.attr("x2", d => d.target.x)
|
|
499
|
+
.attr("y2", d => d.target.y);
|
|
500
|
+
|
|
501
|
+
node
|
|
502
|
+
.attr("cx", d => d.x)
|
|
503
|
+
.attr("cy", d => d.y);
|
|
504
|
+
|
|
505
|
+
label
|
|
506
|
+
.attr("x", d => d.x)
|
|
507
|
+
.attr("y", d => d.y + 20);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
updateStats();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function filterData() {
|
|
514
|
+
const searchTerm = document.getElementById("searchInput").value.toLowerCase();
|
|
515
|
+
const categoryFilter = document.getElementById("categoryFilter").value;
|
|
516
|
+
const minCompatibility = parseInt(document.getElementById("compatibilitySlider").value);
|
|
517
|
+
|
|
518
|
+
// Filter nodes
|
|
519
|
+
currentNodes = graphData.nodes.filter(node => {
|
|
520
|
+
const matchesSearch = !searchTerm ||
|
|
521
|
+
(node.name && node.name.toLowerCase().includes(searchTerm)) ||
|
|
522
|
+
(node.id && node.id.toLowerCase().includes(searchTerm));
|
|
523
|
+
const matchesCategory = !categoryFilter || node.category === categoryFilter;
|
|
524
|
+
return matchesSearch && matchesCategory;
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const nodeIds = new Set(currentNodes.map(n => n.id));
|
|
528
|
+
|
|
529
|
+
// Filter edges
|
|
530
|
+
currentEdges = graphData.edges.filter(edge => {
|
|
531
|
+
const hasValidNodes = nodeIds.has(edge.source) && nodeIds.has(edge.target);
|
|
532
|
+
const meetsCompatibility = (edge.compatibility_score || 0) >= minCompatibility;
|
|
533
|
+
return hasValidNodes && meetsCompatibility;
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
updateVisualization();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function updateStats() {
|
|
540
|
+
const stats = document.getElementById("stats");
|
|
541
|
+
stats.innerHTML = `
|
|
542
|
+
<strong>Current View:</strong><br>
|
|
543
|
+
Nodes: ${currentNodes.length}<br>
|
|
544
|
+
Edges: ${currentEdges.length}<br>
|
|
545
|
+
Density: ${(currentEdges.length / Math.max(1, currentNodes.length * (currentNodes.length - 1))).toFixed(3)}
|
|
546
|
+
`;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function updateLegend() {
|
|
550
|
+
const legend = document.getElementById("legend");
|
|
551
|
+
legend.innerHTML = `
|
|
552
|
+
<strong>Categories:</strong><br>
|
|
553
|
+
${categories.map(cat =>
|
|
554
|
+
`<div><span style="color: ${colorScale(cat)}">●</span> ${cat}</div>`
|
|
555
|
+
).join('')}
|
|
556
|
+
`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function showTooltip(event, d) {
|
|
560
|
+
const tooltip = document.getElementById("tooltip");
|
|
561
|
+
tooltip.innerHTML = `
|
|
562
|
+
<strong>${d.name || d.id}</strong><br>
|
|
563
|
+
Category: ${d.category || 'Unknown'}<br>
|
|
564
|
+
Connections: ${d.connections || 0}<br>
|
|
565
|
+
${d.description ? `<br>${d.description}` : ''}
|
|
566
|
+
`;
|
|
567
|
+
tooltip.style.display = "block";
|
|
568
|
+
tooltip.style.left = (event.pageX + 10) + "px";
|
|
569
|
+
tooltip.style.top = (event.pageY - 10) + "px";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function hideTooltip() {
|
|
573
|
+
document.getElementById("tooltip").style.display = "none";
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function dragstarted(event, d) {
|
|
577
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
578
|
+
d.fx = d.x;
|
|
579
|
+
d.fy = d.y;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function dragged(event, d) {
|
|
583
|
+
d.fx = event.x;
|
|
584
|
+
d.fy = event.y;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function dragended(event, d) {
|
|
588
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
589
|
+
d.fx = null;
|
|
590
|
+
d.fy = null;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Event listeners
|
|
594
|
+
document.getElementById("searchInput").addEventListener("input", filterData);
|
|
595
|
+
document.getElementById("categoryFilter").addEventListener("change", filterData);
|
|
596
|
+
document.getElementById("compatibilitySlider").addEventListener("input", function() {
|
|
597
|
+
document.getElementById("compatibilityValue").textContent = this.value;
|
|
598
|
+
filterData();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Initialize
|
|
602
|
+
updateLegend();
|
|
603
|
+
updateVisualization();
|
|
604
|
+
|
|
605
|
+
// Handle window resize
|
|
606
|
+
window.addEventListener("resize", () => {
|
|
607
|
+
const newWidth = window.innerWidth;
|
|
608
|
+
const newHeight = window.innerHeight - 140;
|
|
609
|
+
svg.attr("width", newWidth).attr("height", newHeight);
|
|
610
|
+
simulation.force("center", d3.forceCenter(newWidth / 2, newHeight / 2));
|
|
611
|
+
simulation.alpha(0.3).restart();
|
|
612
|
+
});
|
|
613
|
+
</script>
|
|
614
|
+
</body>
|
|
615
|
+
</html>
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
@app.route("/")
|
|
619
|
+
def index():
|
|
620
|
+
summary = get_graph_summary(graph_data)
|
|
621
|
+
return render_template_string(
|
|
622
|
+
HTML_TEMPLATE,
|
|
623
|
+
graph_data=graph_data,
|
|
624
|
+
summary=summary,
|
|
625
|
+
theme=theme,
|
|
626
|
+
layout=layout,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
@app.route("/api/graph")
|
|
630
|
+
def api_graph():
|
|
631
|
+
return jsonify(graph_data)
|
|
632
|
+
|
|
633
|
+
@app.route("/api/summary")
|
|
634
|
+
def api_summary():
|
|
635
|
+
return jsonify(get_graph_summary(graph_data))
|
|
636
|
+
|
|
637
|
+
return app
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def export_static_image(graph_data, output_path, format_type="png"):
|
|
641
|
+
"""Export static image of the graph (requires additional dependencies)."""
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
import matplotlib.pyplot as plt
|
|
645
|
+
import networkx as nx
|
|
646
|
+
|
|
647
|
+
except ImportError as e:
|
|
648
|
+
missing_deps = []
|
|
649
|
+
if "matplotlib" in str(e):
|
|
650
|
+
missing_deps.append("matplotlib")
|
|
651
|
+
if "networkx" in str(e):
|
|
652
|
+
missing_deps.append("networkx")
|
|
653
|
+
|
|
654
|
+
error_msg = f"""
|
|
655
|
+
❌ Static export requires additional dependencies: {', '.join(missing_deps)}
|
|
656
|
+
|
|
657
|
+
To install graph visualization dependencies:
|
|
658
|
+
pip install tooluniverse[graph]
|
|
659
|
+
|
|
660
|
+
Or install manually:
|
|
661
|
+
pip install matplotlib networkx
|
|
662
|
+
"""
|
|
663
|
+
raise ImportError(error_msg)
|
|
664
|
+
|
|
665
|
+
# Create NetworkX graph
|
|
666
|
+
G = nx.Graph()
|
|
667
|
+
|
|
668
|
+
# Add nodes
|
|
669
|
+
for node in graph_data["nodes"]:
|
|
670
|
+
G.add_node(node["id"], **node)
|
|
671
|
+
|
|
672
|
+
# Add edges
|
|
673
|
+
for edge in graph_data["edges"]:
|
|
674
|
+
G.add_edge(edge["source"], edge["target"], **edge)
|
|
675
|
+
|
|
676
|
+
# Create visualization
|
|
677
|
+
plt.figure(figsize=(12, 8))
|
|
678
|
+
|
|
679
|
+
# Layout
|
|
680
|
+
if len(G.nodes()) < 100:
|
|
681
|
+
pos = nx.spring_layout(G, k=1, iterations=50)
|
|
682
|
+
else:
|
|
683
|
+
pos = nx.spring_layout(G, k=3, iterations=20)
|
|
684
|
+
|
|
685
|
+
# Draw nodes by category
|
|
686
|
+
categories = set(node.get("category", "unknown") for node in graph_data["nodes"])
|
|
687
|
+
colors = plt.cm.Set3(range(len(categories)))
|
|
688
|
+
category_colors = dict(zip(categories, colors))
|
|
689
|
+
|
|
690
|
+
for category in categories:
|
|
691
|
+
nodes_in_category = [
|
|
692
|
+
node["id"]
|
|
693
|
+
for node in graph_data["nodes"]
|
|
694
|
+
if node.get("category", "unknown") == category
|
|
695
|
+
]
|
|
696
|
+
nx.draw_networkx_nodes(
|
|
697
|
+
G,
|
|
698
|
+
pos,
|
|
699
|
+
nodelist=nodes_in_category,
|
|
700
|
+
node_color=[category_colors[category]],
|
|
701
|
+
node_size=100,
|
|
702
|
+
alpha=0.8,
|
|
703
|
+
label=category,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Draw edges
|
|
707
|
+
nx.draw_networkx_edges(G, pos, alpha=0.3, width=0.5)
|
|
708
|
+
|
|
709
|
+
# Add labels for important nodes
|
|
710
|
+
if len(G.nodes()) < 50:
|
|
711
|
+
nx.draw_networkx_labels(G, pos, font_size=8, font_weight="bold")
|
|
712
|
+
|
|
713
|
+
plt.title(
|
|
714
|
+
f"ToolUniverse Compatibility Graph\n{len(G.nodes())} tools, {len(G.edges())} connections"
|
|
715
|
+
)
|
|
716
|
+
plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
|
|
717
|
+
plt.axis("off")
|
|
718
|
+
plt.tight_layout()
|
|
719
|
+
|
|
720
|
+
# Save
|
|
721
|
+
plt.savefig(output_path, format=format_type, dpi=300, bbox_inches="tight")
|
|
722
|
+
plt.close()
|
|
723
|
+
|
|
724
|
+
return output_path
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def main():
|
|
728
|
+
"""Main function."""
|
|
729
|
+
|
|
730
|
+
args = parse_arguments()
|
|
731
|
+
|
|
732
|
+
print("ToolUniverse Graph Visualizer")
|
|
733
|
+
print("=" * 50)
|
|
734
|
+
print(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
735
|
+
print()
|
|
736
|
+
|
|
737
|
+
# Find graph file
|
|
738
|
+
graph_file = find_graph_file(args.graph_file)
|
|
739
|
+
|
|
740
|
+
if not graph_file:
|
|
741
|
+
print("❌ No graph file found!")
|
|
742
|
+
print("\nSearched in:")
|
|
743
|
+
print(" - ./tool_composition_graph.json")
|
|
744
|
+
print(" - ./graph.json")
|
|
745
|
+
print(" - ./tool_graph.json")
|
|
746
|
+
if args.graph_file:
|
|
747
|
+
print(f" - {args.graph_file}")
|
|
748
|
+
print("\nTo generate a graph, run:")
|
|
749
|
+
print("python generate_tool_graph.py")
|
|
750
|
+
return 1
|
|
751
|
+
|
|
752
|
+
if args.verbose:
|
|
753
|
+
print(f"Loading graph from: {graph_file}")
|
|
754
|
+
|
|
755
|
+
# Load graph data
|
|
756
|
+
try:
|
|
757
|
+
graph_data = load_graph_data(graph_file)
|
|
758
|
+
summary = get_graph_summary(graph_data)
|
|
759
|
+
|
|
760
|
+
print(f"✅ Loaded graph: {graph_file}")
|
|
761
|
+
print(f" Nodes: {summary['node_count']}")
|
|
762
|
+
print(f" Edges: {summary['edge_count']}")
|
|
763
|
+
print(f" Categories: {summary['category_count']}")
|
|
764
|
+
print(f" Density: {summary['edge_density']:.3f}")
|
|
765
|
+
print()
|
|
766
|
+
|
|
767
|
+
except Exception as e:
|
|
768
|
+
print(f"❌ Error loading graph: {e}")
|
|
769
|
+
return 1
|
|
770
|
+
|
|
771
|
+
# Export static image if requested
|
|
772
|
+
if args.export_image:
|
|
773
|
+
try:
|
|
774
|
+
print(f"Exporting static image to {args.export_image}...")
|
|
775
|
+
export_static_image(graph_data, args.export_image, args.format)
|
|
776
|
+
print(f"✅ Image exported: {args.export_image}")
|
|
777
|
+
return 0
|
|
778
|
+
except Exception as e:
|
|
779
|
+
print(f"❌ Export failed: {e}")
|
|
780
|
+
return 1
|
|
781
|
+
|
|
782
|
+
# Launch web visualizer
|
|
783
|
+
if not FLASK_AVAILABLE:
|
|
784
|
+
print(FLASK_IMPORT_ERROR)
|
|
785
|
+
return 1
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
app = create_web_app(graph_data, args.theme, args.layout)
|
|
789
|
+
|
|
790
|
+
print("🚀 Starting web server...")
|
|
791
|
+
print(f" Host: {args.host}")
|
|
792
|
+
print(f" Port: {args.port}")
|
|
793
|
+
print(f" URL: http://{args.host}:{args.port}")
|
|
794
|
+
|
|
795
|
+
if args.open_browser:
|
|
796
|
+
print(" Opening browser...")
|
|
797
|
+
|
|
798
|
+
print("\nPress Ctrl+C to stop the server")
|
|
799
|
+
print()
|
|
800
|
+
|
|
801
|
+
# Open browser if requested
|
|
802
|
+
if args.open_browser:
|
|
803
|
+
import threading
|
|
804
|
+
import time
|
|
805
|
+
|
|
806
|
+
def open_browser():
|
|
807
|
+
time.sleep(1.5) # Wait for server to start
|
|
808
|
+
webbrowser.open(f"http://{args.host}:{args.port}")
|
|
809
|
+
|
|
810
|
+
threading.Thread(target=open_browser, daemon=True).start()
|
|
811
|
+
|
|
812
|
+
# Run the server
|
|
813
|
+
app.run(
|
|
814
|
+
host=args.host,
|
|
815
|
+
port=args.port,
|
|
816
|
+
debug=not args.no_auto_reload,
|
|
817
|
+
use_reloader=not args.no_auto_reload,
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
except KeyboardInterrupt:
|
|
821
|
+
print("\n👋 Server stopped.")
|
|
822
|
+
return 0
|
|
823
|
+
except Exception as e:
|
|
824
|
+
print(f"❌ Server error: {e}")
|
|
825
|
+
return 1
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
if __name__ == "__main__":
|
|
829
|
+
sys.exit(main())
|