napistu 0.1.0__py3-none-any.whl → 0.2.4.dev2__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.
Files changed (59) hide show
  1. napistu/__init__.py +1 -1
  2. napistu/consensus.py +1010 -513
  3. napistu/constants.py +24 -0
  4. napistu/gcs/constants.py +2 -2
  5. napistu/gcs/downloads.py +57 -25
  6. napistu/gcs/utils.py +21 -0
  7. napistu/identifiers.py +105 -6
  8. napistu/ingestion/constants.py +0 -1
  9. napistu/ingestion/obo.py +24 -8
  10. napistu/ingestion/psi_mi.py +20 -5
  11. napistu/ingestion/reactome.py +8 -32
  12. napistu/mcp/__init__.py +69 -0
  13. napistu/mcp/__main__.py +180 -0
  14. napistu/mcp/codebase.py +182 -0
  15. napistu/mcp/codebase_utils.py +298 -0
  16. napistu/mcp/constants.py +72 -0
  17. napistu/mcp/documentation.py +166 -0
  18. napistu/mcp/documentation_utils.py +235 -0
  19. napistu/mcp/execution.py +382 -0
  20. napistu/mcp/profiles.py +73 -0
  21. napistu/mcp/server.py +86 -0
  22. napistu/mcp/tutorials.py +124 -0
  23. napistu/mcp/tutorials_utils.py +230 -0
  24. napistu/mcp/utils.py +47 -0
  25. napistu/mechanism_matching.py +782 -26
  26. napistu/modify/constants.py +41 -0
  27. napistu/modify/curation.py +4 -1
  28. napistu/modify/gaps.py +243 -156
  29. napistu/modify/pathwayannot.py +26 -8
  30. napistu/network/neighborhoods.py +16 -7
  31. napistu/network/net_create.py +209 -54
  32. napistu/network/net_propagation.py +118 -0
  33. napistu/network/net_utils.py +1 -32
  34. napistu/rpy2/netcontextr.py +10 -7
  35. napistu/rpy2/rids.py +7 -5
  36. napistu/sbml_dfs_core.py +46 -29
  37. napistu/sbml_dfs_utils.py +37 -1
  38. napistu/source.py +8 -2
  39. napistu/utils.py +67 -8
  40. napistu-0.2.4.dev2.dist-info/METADATA +84 -0
  41. napistu-0.2.4.dev2.dist-info/RECORD +95 -0
  42. {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/WHEEL +1 -1
  43. tests/conftest.py +11 -5
  44. tests/test_consensus.py +4 -1
  45. tests/test_gaps.py +127 -0
  46. tests/test_gcs.py +3 -2
  47. tests/test_igraph.py +14 -0
  48. tests/test_mcp_documentation_utils.py +13 -0
  49. tests/test_mechanism_matching.py +658 -0
  50. tests/test_net_propagation.py +89 -0
  51. tests/test_net_utils.py +83 -0
  52. tests/test_sbml.py +2 -0
  53. tests/{test_sbml_dfs_create.py → test_sbml_dfs_core.py} +68 -4
  54. tests/test_utils.py +81 -0
  55. napistu-0.1.0.dist-info/METADATA +0 -56
  56. napistu-0.1.0.dist-info/RECORD +0 -77
  57. {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/entry_points.txt +0 -0
  58. {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/licenses/LICENSE +0 -0
  59. {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,382 @@
1
+ """
2
+ Function execution components for the Napistu MCP server.
3
+ """
4
+
5
+ from typing import Dict, List, Any, Optional
6
+ import inspect
7
+
8
+ # Global storage for session context and objects
9
+ _session_context = {}
10
+ _session_objects = {}
11
+
12
+
13
+ async def initialize_components() -> bool:
14
+ """
15
+ Initialize execution components.
16
+
17
+ Returns
18
+ -------
19
+ bool
20
+ True if initialization is successful.
21
+ """
22
+ global _session_context, _session_objects
23
+ import napistu
24
+
25
+ _session_context["napistu"] = napistu
26
+ return True
27
+
28
+
29
+ def register_object(name: str, obj: Any) -> None:
30
+ """
31
+ Register an object with the execution component.
32
+
33
+ Args:
34
+ name: Name to reference the object by
35
+ obj: The object to register
36
+ """
37
+ global _session_objects
38
+ _session_objects[name] = obj
39
+ print(f"Registered object '{name}' with MCP server")
40
+
41
+
42
+ def register_components(mcp, session_context=None, object_registry=None):
43
+ """
44
+ Register function execution components with the MCP server.
45
+
46
+ Args:
47
+ mcp: FastMCP server instance
48
+ session_context: Dictionary of the user's current session (e.g., globals())
49
+ object_registry: Dictionary of named objects to make available
50
+ """
51
+ global _session_context, _session_objects
52
+
53
+ # Initialize context
54
+ if session_context:
55
+ _session_context = session_context
56
+
57
+ if object_registry:
58
+ _session_objects = object_registry
59
+
60
+ import napistu
61
+
62
+ _session_context["napistu"] = napistu
63
+
64
+ # Register resources
65
+ @mcp.resource("napistu-local://registry")
66
+ async def get_registry_summary() -> Dict[str, Any]:
67
+ """
68
+ Get a summary of all objects registered with the server.
69
+ """
70
+ return {
71
+ "object_count": len(_session_objects),
72
+ "object_names": list(_session_objects.keys()),
73
+ "object_types": {
74
+ name: type(obj).__name__ for name, obj in _session_objects.items()
75
+ },
76
+ }
77
+
78
+ @mcp.resource("napistu-local://environment")
79
+ async def get_environment_info() -> Dict[str, Any]:
80
+ """
81
+ Get information about the local Python environment.
82
+ """
83
+ try:
84
+ import napistu
85
+
86
+ napistu_version = getattr(napistu, "__version__", "unknown")
87
+ except ImportError:
88
+ napistu_version = "not installed"
89
+
90
+ import sys
91
+
92
+ return {
93
+ "python_version": sys.version,
94
+ "napistu_version": napistu_version,
95
+ "platform": sys.platform,
96
+ "registered_objects": list(_session_objects.keys()),
97
+ }
98
+
99
+ # Register tools
100
+ @mcp.tool()
101
+ async def list_registry() -> Dict[str, Any]:
102
+ """
103
+ List all objects registered with the server.
104
+
105
+ Returns:
106
+ Dictionary with information about registered objects
107
+ """
108
+ result = {}
109
+
110
+ for name, obj in _session_objects.items():
111
+ obj_type = type(obj).__name__
112
+
113
+ # Get additional info based on object type
114
+ if hasattr(obj, "shape"): # For pandas DataFrame or numpy array
115
+ obj_info = {
116
+ "type": obj_type,
117
+ "shape": str(obj.shape),
118
+ }
119
+ elif hasattr(obj, "__len__"): # For lists, dicts, etc.
120
+ obj_info = {
121
+ "type": obj_type,
122
+ "length": len(obj),
123
+ }
124
+ else:
125
+ obj_info = {
126
+ "type": obj_type,
127
+ }
128
+
129
+ result[name] = obj_info
130
+
131
+ return result
132
+
133
+ @mcp.tool()
134
+ async def describe_object(object_name: str) -> Dict[str, Any]:
135
+ """
136
+ Get detailed information about a registered object.
137
+
138
+ Args:
139
+ object_name: Name of the registered object
140
+
141
+ Returns:
142
+ Dictionary with object information
143
+ """
144
+ if object_name not in _session_objects:
145
+ return {"error": f"Object '{object_name}' not found in registry"}
146
+
147
+ obj = _session_objects[object_name]
148
+ obj_type = type(obj).__name__
149
+
150
+ # Basic info for all objects
151
+ result = {
152
+ "name": object_name,
153
+ "type": obj_type,
154
+ "methods": [],
155
+ "attributes": [],
156
+ }
157
+
158
+ # Add methods and attributes
159
+ for name in dir(obj):
160
+ if name.startswith("_"):
161
+ continue
162
+
163
+ try:
164
+ attr = getattr(obj, name)
165
+
166
+ if callable(attr):
167
+ # Method
168
+ sig = str(inspect.signature(attr))
169
+ doc = inspect.getdoc(attr) or ""
170
+ result["methods"].append(
171
+ {
172
+ "name": name,
173
+ "signature": sig,
174
+ "docstring": doc,
175
+ }
176
+ )
177
+ else:
178
+ # Attribute
179
+ attr_type = type(attr).__name__
180
+ result["attributes"].append(
181
+ {
182
+ "name": name,
183
+ "type": attr_type,
184
+ }
185
+ )
186
+ except Exception:
187
+ # Skip attributes that can't be accessed
188
+ pass
189
+
190
+ return result
191
+
192
+ @mcp.tool()
193
+ async def execute_function(
194
+ function_name: str,
195
+ object_name: Optional[str] = None,
196
+ args: Optional[List] = None,
197
+ kwargs: Optional[Dict] = None,
198
+ ) -> Dict[str, Any]:
199
+ """
200
+ Execute a Napistu function on a registered object.
201
+
202
+ Args:
203
+ function_name: Name of the function to execute
204
+ object_name: Name of the registered object to operate on (if method call)
205
+ args: Positional arguments to pass to the function
206
+ kwargs: Keyword arguments to pass to the function
207
+
208
+ Returns:
209
+ Dictionary with execution results
210
+ """
211
+ args = args or []
212
+ kwargs = kwargs or {}
213
+
214
+ try:
215
+ if object_name:
216
+ # Method call on an object
217
+ if object_name not in _session_objects:
218
+ return {"error": f"Object '{object_name}' not found in registry"}
219
+
220
+ obj = _session_objects[object_name]
221
+
222
+ if not hasattr(obj, function_name):
223
+ return {
224
+ "error": f"Method '{function_name}' not found on object '{object_name}'"
225
+ }
226
+
227
+ func = getattr(obj, function_name)
228
+ result = func(*args, **kwargs)
229
+ else:
230
+ # Global function call
231
+ if function_name in _session_context:
232
+ # Function from session context
233
+ func = _session_context[function_name]
234
+ result = func(*args, **kwargs)
235
+ else:
236
+ # Try to find the function in Napistu
237
+ try:
238
+ import napistu
239
+
240
+ # Split function name by dots for nested modules
241
+ parts = function_name.split(".")
242
+ current = napistu
243
+
244
+ for part in parts[:-1]:
245
+ current = getattr(current, part)
246
+
247
+ func = getattr(current, parts[-1])
248
+ result = func(*args, **kwargs)
249
+ except (ImportError, AttributeError):
250
+ return {"error": f"Function '{function_name}' not found"}
251
+
252
+ # Register result if it's a return value
253
+ if result is not None:
254
+ result_name = f"result_{len(_session_objects) + 1}"
255
+ _session_objects[result_name] = result
256
+
257
+ # Basic type conversion for JSON serialization
258
+ if hasattr(result, "to_dict"):
259
+ # For pandas DataFrame or similar
260
+ return {
261
+ "success": True,
262
+ "result_name": result_name,
263
+ "result_type": type(result).__name__,
264
+ "result_preview": (
265
+ result.to_dict()
266
+ if hasattr(result, "__len__") and len(result) < 10
267
+ else "Result too large to preview"
268
+ ),
269
+ }
270
+ elif hasattr(result, "to_json"):
271
+ # For objects with JSON serialization
272
+ return {
273
+ "success": True,
274
+ "result_name": result_name,
275
+ "result_type": type(result).__name__,
276
+ "result_preview": result.to_json(),
277
+ }
278
+ elif hasattr(result, "__dict__"):
279
+ # For custom objects
280
+ return {
281
+ "success": True,
282
+ "result_name": result_name,
283
+ "result_type": type(result).__name__,
284
+ "result_preview": str(result),
285
+ }
286
+ else:
287
+ # For simple types
288
+ return {
289
+ "success": True,
290
+ "result_name": result_name,
291
+ "result_type": type(result).__name__,
292
+ "result_preview": str(result),
293
+ }
294
+ else:
295
+ return {
296
+ "success": True,
297
+ "result": None,
298
+ }
299
+ except Exception as e:
300
+ import traceback
301
+
302
+ return {
303
+ "error": str(e),
304
+ "traceback": traceback.format_exc(),
305
+ }
306
+
307
+ @mcp.tool()
308
+ async def search_paths(
309
+ source_node: str,
310
+ target_node: str,
311
+ network_object: str,
312
+ max_depth: int = 3,
313
+ ) -> Dict[str, Any]:
314
+ """
315
+ Find paths between two nodes in a network.
316
+
317
+ Args:
318
+ source_node: Source node identifier
319
+ target_node: Target node identifier
320
+ network_object: Name of the registered network object
321
+ max_depth: Maximum path length
322
+
323
+ Returns:
324
+ Dictionary with paths found
325
+ """
326
+ if network_object not in _session_objects:
327
+ return {"error": f"Network object '{network_object}' not found in registry"}
328
+
329
+ network = _session_objects[network_object]
330
+
331
+ try:
332
+ # Import necessary modules
333
+ import napistu
334
+
335
+ # Check if the object is a valid network type
336
+ if hasattr(network, "find_paths"):
337
+ # Direct method call
338
+ paths = network.find_paths(
339
+ source_node, target_node, max_depth=max_depth
340
+ )
341
+ elif hasattr(napistu.graph, "find_paths"):
342
+ # Function call
343
+ paths = napistu.graph.find_paths(
344
+ network, source_node, target_node, max_depth=max_depth
345
+ )
346
+ else:
347
+ return {"error": "Could not find appropriate path-finding function"}
348
+
349
+ # Register result
350
+ result_name = f"paths_{len(_session_objects) + 1}"
351
+ _session_objects[result_name] = paths
352
+
353
+ # Return results
354
+ if hasattr(paths, "to_dict"):
355
+ return {
356
+ "success": True,
357
+ "result_name": result_name,
358
+ "paths_found": (
359
+ len(paths) if hasattr(paths, "__len__") else "unknown"
360
+ ),
361
+ "result_preview": (
362
+ paths.to_dict()
363
+ if hasattr(paths, "__len__") and len(paths) < 10
364
+ else "Result too large to preview"
365
+ ),
366
+ }
367
+ else:
368
+ return {
369
+ "success": True,
370
+ "result_name": result_name,
371
+ "paths_found": (
372
+ len(paths) if hasattr(paths, "__len__") else "unknown"
373
+ ),
374
+ "result_preview": str(paths),
375
+ }
376
+ except Exception as e:
377
+ import traceback
378
+
379
+ return {
380
+ "error": str(e),
381
+ "traceback": traceback.format_exc(),
382
+ }
@@ -0,0 +1,73 @@
1
+ from typing import Dict, Any
2
+
3
+
4
+ class ServerProfile:
5
+ """Base profile for MCP server configuration."""
6
+
7
+ def __init__(self, **kwargs):
8
+ self.config = {
9
+ # Default configuration
10
+ "server_name": "napistu-mcp",
11
+ "enable_documentation": False,
12
+ "enable_execution": False,
13
+ "enable_codebase": False,
14
+ "enable_tutorials": False,
15
+ "session_context": None,
16
+ "object_registry": None,
17
+ "tutorials_path": None,
18
+ }
19
+ # Override with provided kwargs
20
+ self.config.update(kwargs)
21
+
22
+ def get_config(self) -> Dict[str, Any]:
23
+ """Return the configuration dictionary."""
24
+ return self.config.copy()
25
+
26
+ def update(self, **kwargs) -> "ServerProfile":
27
+ """Update profile with additional configuration."""
28
+ new_profile = ServerProfile(**self.config)
29
+ new_profile.config.update(kwargs)
30
+ return new_profile
31
+
32
+
33
+ # Pre-defined profiles
34
+ LOCAL_PROFILE = ServerProfile(server_name="napistu-local", enable_execution=True)
35
+
36
+ REMOTE_PROFILE = ServerProfile(
37
+ server_name="napistu-docs",
38
+ enable_documentation=True,
39
+ enable_codebase=True,
40
+ enable_tutorials=True,
41
+ )
42
+
43
+ FULL_PROFILE = ServerProfile(
44
+ server_name="napistu-full",
45
+ enable_documentation=True,
46
+ enable_codebase=True,
47
+ enable_execution=True,
48
+ enable_tutorials=True,
49
+ )
50
+
51
+
52
+ def get_profile(profile_name: str, **overrides) -> ServerProfile:
53
+ """
54
+ Get a predefined profile with optional overrides.
55
+
56
+ Args:
57
+ profile_name: Name of the profile ('local', 'remote', or 'full')
58
+ **overrides: Configuration overrides
59
+
60
+ Returns:
61
+ ServerProfile instance
62
+ """
63
+ profiles = {
64
+ "local": LOCAL_PROFILE,
65
+ "remote": REMOTE_PROFILE,
66
+ "full": FULL_PROFILE,
67
+ }
68
+
69
+ if profile_name not in profiles:
70
+ raise ValueError(f"Unknown profile: {profile_name}")
71
+
72
+ # Return a copy of the profile with overrides
73
+ return profiles[profile_name].update(**overrides)
napistu/mcp/server.py ADDED
@@ -0,0 +1,86 @@
1
+ """
2
+ Core MCP server implementation for Napistu.
3
+ """
4
+
5
+ import logging
6
+
7
+ from mcp.server import FastMCP
8
+
9
+ from napistu.mcp import codebase
10
+ from napistu.mcp import documentation
11
+ from napistu.mcp import execution
12
+ from napistu.mcp import tutorials
13
+
14
+ from napistu.mcp.profiles import ServerProfile
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def create_server(profile: ServerProfile, **kwargs) -> FastMCP:
20
+ """
21
+ Create an MCP server based on a profile configuration.
22
+
23
+ Parameters
24
+ ----------
25
+ profile : ServerProfile
26
+ Server profile to use. All configuration must be set in the profile.
27
+ **kwargs
28
+ Additional arguments to pass to the FastMCP constructor such as host and port.
29
+
30
+ Returns
31
+ -------
32
+ FastMCP
33
+ Configured FastMCP server instance.
34
+ """
35
+
36
+ config = profile.get_config()
37
+
38
+ # Create the server with FastMCP-specific parameters
39
+ # Pass all kwargs directly to the FastMCP constructor
40
+ mcp = FastMCP(config["server_name"], **kwargs)
41
+
42
+ if config["enable_documentation"]:
43
+ logger.info("Registering documentation components")
44
+ documentation.register_components(mcp)
45
+ if config["enable_codebase"]:
46
+ logger.info("Registering codebase components")
47
+ codebase.register_components(mcp)
48
+ if config["enable_execution"]:
49
+ logger.info("Registering execution components")
50
+ execution.register_components(
51
+ mcp,
52
+ session_context=config["session_context"],
53
+ object_registry=config["object_registry"],
54
+ )
55
+ if config["enable_tutorials"]:
56
+ logger.info("Registering tutorials components")
57
+ tutorials.register_components(mcp)
58
+ return mcp
59
+
60
+
61
+ async def initialize_components(profile: ServerProfile) -> None:
62
+ """
63
+ Asynchronously initialize all enabled components for the MCP server, using the provided ServerProfile.
64
+
65
+ Parameters
66
+ ----------
67
+ profile : ServerProfile
68
+ The profile whose configuration determines which components to initialize.
69
+
70
+ Returns
71
+ -------
72
+ None
73
+ """
74
+ config = profile.get_config()
75
+ if config["enable_documentation"]:
76
+ logger.info("Initializing documentation components")
77
+ await documentation.initialize_components()
78
+ if config["enable_codebase"]:
79
+ logger.info("Initializing codebase components")
80
+ await codebase.initialize_components()
81
+ if config["enable_tutorials"]:
82
+ logger.info("Initializing tutorials components")
83
+ await tutorials.initialize_components()
84
+ if config["enable_execution"]:
85
+ logger.info("Initializing execution components")
86
+ await execution.initialize_components()
@@ -0,0 +1,124 @@
1
+ """
2
+ Tutorial components for the Napistu MCP server.
3
+ """
4
+
5
+ from typing import Dict, List, Any
6
+ import logging
7
+
8
+ from fastmcp import FastMCP
9
+
10
+ from napistu.mcp import tutorials_utils
11
+ from napistu.mcp import utils as mcp_utils
12
+ from napistu.mcp.constants import TUTORIALS
13
+ from napistu.mcp.constants import TUTORIAL_URLS
14
+ from napistu.mcp.constants import TOOL_VARS
15
+
16
+ # Global cache for tutorial content
17
+ _tutorial_cache: Dict[str, Dict[str, str]] = {
18
+ TUTORIALS.TUTORIALS: {},
19
+ }
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ async def initialize_components() -> bool:
25
+ """
26
+ Initialize tutorial components by preloading all tutorials into the cache.
27
+
28
+ Returns
29
+ -------
30
+ bool
31
+ True if initialization is successful.
32
+ """
33
+ global _tutorial_cache
34
+ for k, v in TUTORIAL_URLS.items():
35
+ _tutorial_cache[TUTORIALS.TUTORIALS][k] = (
36
+ await tutorials_utils.get_tutorial_markdown(k)
37
+ )
38
+ return True
39
+
40
+
41
+ def register_components(mcp: FastMCP) -> None:
42
+ """
43
+ Register tutorial components with the MCP server.
44
+
45
+ Parameters
46
+ ----------
47
+ mcp : FastMCP
48
+ FastMCP server instance.
49
+ """
50
+
51
+ # Register resources
52
+ @mcp.resource("napistu://tutorials/index")
53
+ async def get_tutorial_index() -> List[Dict[str, Any]]:
54
+ """
55
+ Get the index of all available tutorials.
56
+
57
+ Returns
58
+ -------
59
+ List[dict]
60
+ List of dictionaries with tutorial IDs and URLs.
61
+ """
62
+ return [
63
+ {"id": tutorial_id, "url": url}
64
+ for tutorial_id, url in TUTORIAL_URLS.items()
65
+ ]
66
+
67
+ @mcp.resource("napistu://tutorials/content/{tutorial_id}")
68
+ async def get_tutorial_content_resource(tutorial_id: str) -> Dict[str, Any]:
69
+ """
70
+ Get the content of a specific tutorial as markdown.
71
+
72
+ Parameters
73
+ ----------
74
+ tutorial_id : str
75
+ ID of the tutorial.
76
+
77
+ Returns
78
+ -------
79
+ dict
80
+ Dictionary with markdown content and format.
81
+
82
+ Raises
83
+ ------
84
+ Exception
85
+ If the tutorial cannot be loaded.
86
+ """
87
+ content = _tutorial_cache[TUTORIALS.TUTORIALS].get(tutorial_id)
88
+ if content is None:
89
+ try:
90
+ content = await tutorials_utils.get_tutorial_markdown(tutorial_id)
91
+ _tutorial_cache[TUTORIALS.TUTORIALS][tutorial_id] = content
92
+ except Exception as e:
93
+ logger.error(f"Tutorial {tutorial_id} could not be loaded: {e}")
94
+ raise
95
+ return {
96
+ "content": content,
97
+ "format": "markdown",
98
+ }
99
+
100
+ @mcp.tool()
101
+ async def search_tutorials(query: str) -> List[Dict[str, Any]]:
102
+ """
103
+ Search tutorials for a specific query.
104
+
105
+ Parameters
106
+ ----------
107
+ query : str
108
+ Search term.
109
+
110
+ Returns
111
+ -------
112
+ List[dict]
113
+ List of matching tutorials with metadata and snippet.
114
+ """
115
+ results: List[Dict[str, Any]] = []
116
+ for tutorial_id, content in _tutorial_cache[TUTORIALS.TUTORIALS].items():
117
+ if query.lower() in content.lower():
118
+ results.append(
119
+ {
120
+ TOOL_VARS.ID: tutorial_id,
121
+ TOOL_VARS.SNIPPET: mcp_utils.get_snippet(content, query),
122
+ }
123
+ )
124
+ return results