tooluniverse 0.2.0__py3-none-any.whl → 1.0.1__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.

Files changed (186) hide show
  1. tooluniverse/__init__.py +340 -4
  2. tooluniverse/admetai_tool.py +84 -0
  3. tooluniverse/agentic_tool.py +563 -0
  4. tooluniverse/alphafold_tool.py +96 -0
  5. tooluniverse/base_tool.py +129 -6
  6. tooluniverse/boltz_tool.py +207 -0
  7. tooluniverse/chem_tool.py +192 -0
  8. tooluniverse/compose_scripts/__init__.py +1 -0
  9. tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
  10. tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
  11. tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
  12. tooluniverse/compose_scripts/literature_tool.py +34 -0
  13. tooluniverse/compose_scripts/output_summarizer.py +279 -0
  14. tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
  15. tooluniverse/compose_scripts/tool_discover.py +705 -0
  16. tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
  17. tooluniverse/compose_tool.py +371 -0
  18. tooluniverse/ctg_tool.py +1002 -0
  19. tooluniverse/custom_tool.py +81 -0
  20. tooluniverse/dailymed_tool.py +108 -0
  21. tooluniverse/data/admetai_tools.json +155 -0
  22. tooluniverse/data/adverse_event_tools.json +108 -0
  23. tooluniverse/data/agentic_tools.json +1156 -0
  24. tooluniverse/data/alphafold_tools.json +87 -0
  25. tooluniverse/data/boltz_tools.json +9 -0
  26. tooluniverse/data/chembl_tools.json +16 -0
  27. tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
  28. tooluniverse/data/compose_tools.json +202 -0
  29. tooluniverse/data/dailymed_tools.json +70 -0
  30. tooluniverse/data/dataset_tools.json +646 -0
  31. tooluniverse/data/disease_target_score_tools.json +712 -0
  32. tooluniverse/data/efo_tools.json +17 -0
  33. tooluniverse/data/embedding_tools.json +319 -0
  34. tooluniverse/data/enrichr_tools.json +31 -0
  35. tooluniverse/data/europe_pmc_tools.json +22 -0
  36. tooluniverse/data/expert_feedback_tools.json +10 -0
  37. tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
  38. tooluniverse/data/fda_drug_labeling_tools.json +1 -1
  39. tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
  40. tooluniverse/data/finder_tools.json +209 -0
  41. tooluniverse/data/gene_ontology_tools.json +113 -0
  42. tooluniverse/data/gwas_tools.json +1082 -0
  43. tooluniverse/data/hpa_tools.json +333 -0
  44. tooluniverse/data/humanbase_tools.json +47 -0
  45. tooluniverse/data/idmap_tools.json +74 -0
  46. tooluniverse/data/mcp_client_tools_example.json +113 -0
  47. tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
  48. tooluniverse/data/medlineplus_tools.json +141 -0
  49. tooluniverse/data/monarch_tools.json +1 -1
  50. tooluniverse/data/openalex_tools.json +36 -0
  51. tooluniverse/data/opentarget_tools.json +1 -1
  52. tooluniverse/data/output_summarization_tools.json +101 -0
  53. tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
  54. tooluniverse/data/packages/categorized_tools.txt +206 -0
  55. tooluniverse/data/packages/cheminformatics_tools.json +347 -0
  56. tooluniverse/data/packages/earth_sciences_tools.json +74 -0
  57. tooluniverse/data/packages/genomics_tools.json +776 -0
  58. tooluniverse/data/packages/image_processing_tools.json +38 -0
  59. tooluniverse/data/packages/machine_learning_tools.json +789 -0
  60. tooluniverse/data/packages/neuroscience_tools.json +62 -0
  61. tooluniverse/data/packages/original_tools.txt +0 -0
  62. tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
  63. tooluniverse/data/packages/scientific_computing_tools.json +560 -0
  64. tooluniverse/data/packages/single_cell_tools.json +453 -0
  65. tooluniverse/data/packages/structural_biology_tools.json +396 -0
  66. tooluniverse/data/packages/visualization_tools.json +399 -0
  67. tooluniverse/data/pubchem_tools.json +215 -0
  68. tooluniverse/data/pubtator_tools.json +68 -0
  69. tooluniverse/data/rcsb_pdb_tools.json +1332 -0
  70. tooluniverse/data/reactome_tools.json +19 -0
  71. tooluniverse/data/semantic_scholar_tools.json +26 -0
  72. tooluniverse/data/special_tools.json +2 -25
  73. tooluniverse/data/tool_composition_tools.json +88 -0
  74. tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
  75. tooluniverse/data/txagent_client_tools.json +9 -0
  76. tooluniverse/data/uniprot_tools.json +211 -0
  77. tooluniverse/data/url_fetch_tools.json +94 -0
  78. tooluniverse/data/uspto_downloader_tools.json +9 -0
  79. tooluniverse/data/uspto_tools.json +811 -0
  80. tooluniverse/data/xml_tools.json +3275 -0
  81. tooluniverse/dataset_tool.py +296 -0
  82. tooluniverse/default_config.py +165 -0
  83. tooluniverse/efo_tool.py +42 -0
  84. tooluniverse/embedding_database.py +630 -0
  85. tooluniverse/embedding_sync.py +396 -0
  86. tooluniverse/enrichr_tool.py +266 -0
  87. tooluniverse/europe_pmc_tool.py +52 -0
  88. tooluniverse/execute_function.py +1775 -95
  89. tooluniverse/extended_hooks.py +444 -0
  90. tooluniverse/gene_ontology_tool.py +194 -0
  91. tooluniverse/graphql_tool.py +158 -36
  92. tooluniverse/gwas_tool.py +358 -0
  93. tooluniverse/hpa_tool.py +1645 -0
  94. tooluniverse/humanbase_tool.py +389 -0
  95. tooluniverse/logging_config.py +254 -0
  96. tooluniverse/mcp_client_tool.py +764 -0
  97. tooluniverse/mcp_integration.py +413 -0
  98. tooluniverse/mcp_tool_registry.py +925 -0
  99. tooluniverse/medlineplus_tool.py +337 -0
  100. tooluniverse/openalex_tool.py +228 -0
  101. tooluniverse/openfda_adv_tool.py +283 -0
  102. tooluniverse/openfda_tool.py +393 -160
  103. tooluniverse/output_hook.py +1122 -0
  104. tooluniverse/package_tool.py +195 -0
  105. tooluniverse/pubchem_tool.py +158 -0
  106. tooluniverse/pubtator_tool.py +168 -0
  107. tooluniverse/rcsb_pdb_tool.py +38 -0
  108. tooluniverse/reactome_tool.py +108 -0
  109. tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
  110. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
  111. tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
  112. tooluniverse/remote/expert_feedback/simple_test.py +23 -0
  113. tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
  114. tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
  115. tooluniverse/remote/immune_compass/compass_tool.py +327 -0
  116. tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
  117. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
  118. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
  119. tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
  120. tooluniverse/remote_tool.py +99 -0
  121. tooluniverse/restful_tool.py +53 -30
  122. tooluniverse/scripts/generate_tool_graph.py +408 -0
  123. tooluniverse/scripts/visualize_tool_graph.py +829 -0
  124. tooluniverse/semantic_scholar_tool.py +62 -0
  125. tooluniverse/smcp.py +2452 -0
  126. tooluniverse/smcp_server.py +975 -0
  127. tooluniverse/test/mcp_server_test.py +0 -0
  128. tooluniverse/test/test_admetai_tool.py +370 -0
  129. tooluniverse/test/test_agentic_tool.py +129 -0
  130. tooluniverse/test/test_alphafold_tool.py +71 -0
  131. tooluniverse/test/test_chem_tool.py +37 -0
  132. tooluniverse/test/test_compose_lieraturereview.py +63 -0
  133. tooluniverse/test/test_compose_tool.py +448 -0
  134. tooluniverse/test/test_dailymed.py +69 -0
  135. tooluniverse/test/test_dataset_tool.py +200 -0
  136. tooluniverse/test/test_disease_target_score.py +56 -0
  137. tooluniverse/test/test_drugbank_filter_examples.py +179 -0
  138. tooluniverse/test/test_efo.py +31 -0
  139. tooluniverse/test/test_enrichr_tool.py +21 -0
  140. tooluniverse/test/test_europe_pmc_tool.py +20 -0
  141. tooluniverse/test/test_fda_adv.py +95 -0
  142. tooluniverse/test/test_fda_drug_labeling.py +91 -0
  143. tooluniverse/test/test_gene_ontology_tools.py +66 -0
  144. tooluniverse/test/test_gwas_tool.py +139 -0
  145. tooluniverse/test/test_hpa.py +625 -0
  146. tooluniverse/test/test_humanbase_tool.py +20 -0
  147. tooluniverse/test/test_idmap_tools.py +61 -0
  148. tooluniverse/test/test_mcp_server.py +211 -0
  149. tooluniverse/test/test_mcp_tool.py +247 -0
  150. tooluniverse/test/test_medlineplus.py +220 -0
  151. tooluniverse/test/test_openalex_tool.py +32 -0
  152. tooluniverse/test/test_opentargets.py +28 -0
  153. tooluniverse/test/test_pubchem_tool.py +116 -0
  154. tooluniverse/test/test_pubtator_tool.py +37 -0
  155. tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
  156. tooluniverse/test/test_reactome.py +54 -0
  157. tooluniverse/test/test_semantic_scholar_tool.py +24 -0
  158. tooluniverse/test/test_software_tools.py +147 -0
  159. tooluniverse/test/test_tool_description_optimizer.py +49 -0
  160. tooluniverse/test/test_tool_finder.py +26 -0
  161. tooluniverse/test/test_tool_finder_llm.py +252 -0
  162. tooluniverse/test/test_tools_find.py +195 -0
  163. tooluniverse/test/test_uniprot_tools.py +74 -0
  164. tooluniverse/test/test_uspto_tool.py +72 -0
  165. tooluniverse/test/test_xml_tool.py +113 -0
  166. tooluniverse/tool_finder_embedding.py +267 -0
  167. tooluniverse/tool_finder_keyword.py +693 -0
  168. tooluniverse/tool_finder_llm.py +699 -0
  169. tooluniverse/tool_graph_web_ui.py +955 -0
  170. tooluniverse/tool_registry.py +416 -0
  171. tooluniverse/uniprot_tool.py +155 -0
  172. tooluniverse/url_tool.py +253 -0
  173. tooluniverse/uspto_tool.py +240 -0
  174. tooluniverse/utils.py +369 -41
  175. tooluniverse/xml_tool.py +369 -0
  176. tooluniverse-1.0.1.dist-info/METADATA +387 -0
  177. tooluniverse-1.0.1.dist-info/RECORD +182 -0
  178. tooluniverse-1.0.1.dist-info/entry_points.txt +9 -0
  179. tooluniverse/generate_mcp_tools.py +0 -113
  180. tooluniverse/mcp_server.py +0 -3340
  181. tooluniverse-0.2.0.dist-info/METADATA +0 -139
  182. tooluniverse-0.2.0.dist-info/RECORD +0 -21
  183. tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
  184. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/WHEEL +0 -0
  185. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.dist-info}/licenses/LICENSE +0 -0
  186. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.1.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())