rowan-mcp 1.0.2__py3-none-any.whl → 2.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 rowan-mcp might be problematic. Click here for more details.

Files changed (70) hide show
  1. rowan_mcp/__init__.py +1 -1
  2. rowan_mcp/__main__.py +3 -5
  3. rowan_mcp/functions_v2/BENCHMARK.md +86 -0
  4. rowan_mcp/functions_v2/molecule_lookup.py +232 -0
  5. rowan_mcp/functions_v2/protein_management.py +141 -0
  6. rowan_mcp/functions_v2/submit_basic_calculation_workflow.py +195 -0
  7. rowan_mcp/functions_v2/submit_conformer_search_workflow.py +158 -0
  8. rowan_mcp/functions_v2/submit_descriptors_workflow.py +52 -0
  9. rowan_mcp/functions_v2/submit_docking_workflow.py +244 -0
  10. rowan_mcp/functions_v2/submit_fukui_workflow.py +114 -0
  11. rowan_mcp/functions_v2/submit_irc_workflow.py +58 -0
  12. rowan_mcp/functions_v2/submit_macropka_workflow.py +99 -0
  13. rowan_mcp/functions_v2/submit_pka_workflow.py +72 -0
  14. rowan_mcp/functions_v2/submit_protein_cofolding_workflow.py +88 -0
  15. rowan_mcp/functions_v2/submit_redox_potential_workflow.py +55 -0
  16. rowan_mcp/functions_v2/submit_scan_workflow.py +82 -0
  17. rowan_mcp/functions_v2/submit_solubility_workflow.py +157 -0
  18. rowan_mcp/functions_v2/submit_tautomer_search_workflow.py +51 -0
  19. rowan_mcp/functions_v2/workflow_management_v2.py +382 -0
  20. rowan_mcp/server.py +109 -144
  21. rowan_mcp/tests/basic_calculation_from_json.py +0 -0
  22. rowan_mcp/tests/basic_calculation_with_constraint.py +33 -0
  23. rowan_mcp/tests/basic_calculation_with_solvent.py +0 -0
  24. rowan_mcp/tests/bde.py +37 -0
  25. rowan_mcp/tests/benchmark_queries.md +120 -0
  26. rowan_mcp/tests/cofolding_screen.py +131 -0
  27. rowan_mcp/tests/conformer_dependent_redox.py +37 -0
  28. rowan_mcp/tests/conformers.py +31 -0
  29. rowan_mcp/tests/data.json +189 -0
  30. rowan_mcp/tests/docking_screen.py +157 -0
  31. rowan_mcp/tests/irc.py +24 -0
  32. rowan_mcp/tests/macropka.py +13 -0
  33. rowan_mcp/tests/multistage_opt.py +13 -0
  34. rowan_mcp/tests/optimization.py +21 -0
  35. rowan_mcp/tests/phenol_pka.py +36 -0
  36. rowan_mcp/tests/pka.py +36 -0
  37. rowan_mcp/tests/protein_cofolding.py +17 -0
  38. rowan_mcp/tests/scan.py +28 -0
  39. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +41 -33
  40. rowan_mcp-2.0.0.dist-info/RECORD +42 -0
  41. rowan_mcp/functions/admet.py +0 -94
  42. rowan_mcp/functions/bde.py +0 -113
  43. rowan_mcp/functions/calculation_retrieve.py +0 -89
  44. rowan_mcp/functions/conformers.py +0 -80
  45. rowan_mcp/functions/descriptors.py +0 -92
  46. rowan_mcp/functions/docking.py +0 -340
  47. rowan_mcp/functions/docking_enhanced.py +0 -174
  48. rowan_mcp/functions/electronic_properties.py +0 -205
  49. rowan_mcp/functions/folder_management.py +0 -137
  50. rowan_mcp/functions/fukui.py +0 -219
  51. rowan_mcp/functions/hydrogen_bond_basicity.py +0 -94
  52. rowan_mcp/functions/irc.py +0 -125
  53. rowan_mcp/functions/macropka.py +0 -120
  54. rowan_mcp/functions/molecular_converter.py +0 -423
  55. rowan_mcp/functions/molecular_dynamics.py +0 -191
  56. rowan_mcp/functions/molecule_lookup.py +0 -57
  57. rowan_mcp/functions/multistage_opt.py +0 -171
  58. rowan_mcp/functions/pdb_handler.py +0 -200
  59. rowan_mcp/functions/pka.py +0 -88
  60. rowan_mcp/functions/redox_potential.py +0 -352
  61. rowan_mcp/functions/scan.py +0 -536
  62. rowan_mcp/functions/scan_analyzer.py +0 -347
  63. rowan_mcp/functions/solubility.py +0 -277
  64. rowan_mcp/functions/spin_states.py +0 -747
  65. rowan_mcp/functions/system_management.py +0 -368
  66. rowan_mcp/functions/tautomers.py +0 -91
  67. rowan_mcp/functions/workflow_management.py +0 -422
  68. rowan_mcp-1.0.2.dist-info/RECORD +0 -34
  69. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
  70. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,191 +0,0 @@
1
- """
2
- Rowan molecular dynamics function for MCP tool integration.
3
- """
4
-
5
- from typing import Any, Dict, List, Optional
6
- import rowan
7
-
8
- def rowan_molecular_dynamics(
9
- name: str,
10
- molecule: str,
11
- ensemble: str = "nvt",
12
- initialization: str = "random",
13
- timestep: float = 1.0,
14
- num_steps: int = 500,
15
- save_interval: int = 10,
16
- temperature: float = 300.0,
17
- pressure: Optional[float] = None,
18
- langevin_thermostat_timescale: float = 100.0,
19
- berendsen_barostat_timescale: float = 1000.0,
20
- constraints: Optional[List[Dict[str, Any]]] = None,
21
- confining_constraint: Optional[Dict[str, Any]] = None,
22
- # Calculation settings parameters
23
- method: Optional[str] = None,
24
- basis_set: Optional[str] = None,
25
- engine: Optional[str] = None,
26
- charge: int = 0,
27
- multiplicity: int = 1,
28
- # Workflow control parameters
29
- folder_uuid: Optional[str] = None,
30
- blocking: bool = True,
31
- ping_interval: int = 5
32
- ) -> str:
33
- """Run molecular dynamics simulations following Rowan's MolecularDynamicsWorkflow.
34
-
35
- Performs MD simulations to study molecular dynamics, conformational sampling,
36
- and thermal properties using various thermodynamic ensembles.
37
-
38
- Args:
39
- name: Name for the calculation
40
- molecule: Molecule SMILES string or common name
41
- ensemble: Thermodynamic ensemble ("nvt", "npt", "nve")
42
- initialization: Initial velocities ("random", "quasiclassical", "read")
43
- timestep: Integration timestep in femtoseconds
44
- num_steps: Number of MD steps to run
45
- save_interval: Save trajectory every N steps
46
- temperature: Temperature in Kelvin
47
- pressure: Pressure in atm (required for NPT)
48
- langevin_thermostat_timescale: Thermostat coupling timescale in fs
49
- berendsen_barostat_timescale: Barostat coupling timescale in fs
50
- constraints: List of pairwise harmonic constraints
51
- confining_constraint: Spherical harmonic constraint
52
- method: QM method for force calculation
53
- basis_set: Basis set for force calculation
54
- engine: Computational engine for force calculation
55
- charge: Molecular charge
56
- multiplicity: Spin multiplicity
57
- folder_uuid: Optional folder UUID for organization
58
- blocking: Whether to wait for completion
59
- ping_interval: Check status interval in seconds
60
-
61
- Example:
62
- result = rowan_molecular_dynamics(
63
- name="ethanol_md_simulation",
64
- molecule="ethanol",
65
- ensemble="NVT",
66
- temperature=298,
67
- num_steps=1000,
68
- blocking=False
69
- )
70
-
71
- Returns:
72
- Molecular dynamics workflow result
73
- """
74
- # Parameter validation
75
- valid_ensembles = ["nvt", "npt", "nve"]
76
- valid_initializations = ["random", "quasiclassical", "read"]
77
-
78
- # Validate ensemble
79
- ensemble_lower = ensemble.lower()
80
- if ensemble_lower not in valid_ensembles:
81
- return f" Error: Invalid ensemble '{ensemble}'. Valid options: {', '.join(valid_ensembles)}"
82
-
83
- # Validate initialization
84
- initialization_lower = initialization.lower()
85
- if initialization_lower not in valid_initializations:
86
- return f" Error: Invalid initialization '{initialization}'. Valid options: {', '.join(valid_initializations)}"
87
-
88
- # Validate numeric parameters
89
- if timestep <= 0:
90
- return f" Error: timestep must be positive (got {timestep})"
91
- if num_steps <= 0:
92
- return f" Error: num_steps must be positive (got {num_steps})"
93
- if save_interval <= 0:
94
- return f" Error: save_interval must be positive (got {save_interval})"
95
- if temperature <= 0:
96
- return f" Error: temperature must be positive (got {temperature})"
97
-
98
- # Validate NPT ensemble requirements
99
- if ensemble_lower == "npt" and pressure is None:
100
- return f" Error: NPT ensemble requires pressure to be specified"
101
- if pressure is not None and pressure <= 0:
102
- return f" Error: pressure must be positive (got {pressure})"
103
-
104
- # Convert molecule name to SMILES using lookup system
105
- try:
106
- from .molecule_lookup import get_lookup_instance
107
- lookup = get_lookup_instance()
108
- smiles, source, metadata = lookup.get_smiles(molecule)
109
- if smiles:
110
- resolved_smiles = smiles
111
- else:
112
- resolved_smiles = molecule # Fallback to original
113
- except Exception:
114
- resolved_smiles = molecule # Fallback if lookup fails
115
-
116
- # Apply smart defaults for MD calculations
117
- if engine is None:
118
- engine = "xtb" # Default to xTB for fast MD forces
119
- if method is None and engine.lower() == "xtb":
120
- method = "gfn2-xtb" # Default xTB method
121
- elif method is None and engine.lower() != "xtb":
122
- method = "b3lyp" # Default DFT method for other engines
123
- if basis_set is None and engine.lower() != "xtb":
124
- basis_set = "def2-svp" # Default basis set for non-xTB engines
125
-
126
- # Build MD settings
127
- md_settings = {
128
- "ensemble": ensemble_lower,
129
- "initialization": initialization_lower,
130
- "timestep": timestep,
131
- "num_steps": num_steps,
132
- "save_interval": save_interval,
133
- "temperature": temperature,
134
- "langevin_thermostat_timescale": langevin_thermostat_timescale,
135
- "berendsen_barostat_timescale": berendsen_barostat_timescale,
136
- }
137
-
138
- # Add optional fields if provided
139
- if pressure is not None:
140
- md_settings["pressure"] = pressure
141
-
142
- if constraints:
143
- md_settings["constraints"] = constraints
144
-
145
- if confining_constraint:
146
- md_settings["confining_constraint"] = confining_constraint
147
-
148
- # Build calc_settings
149
- calc_settings = {
150
- "charge": charge,
151
- "multiplicity": multiplicity,
152
- "engine": engine.lower()
153
- }
154
-
155
- # Add method if specified
156
- if method:
157
- calc_settings["method"] = method.lower()
158
-
159
- # Add basis_set if specified (not needed for xTB)
160
- if basis_set and engine.lower() != "xtb":
161
- calc_settings["basis_set"] = basis_set.lower()
162
-
163
- # Build parameters for Rowan API
164
- workflow_params = {
165
- "name": name,
166
- "molecule": resolved_smiles,
167
- "workflow_type": "molecular_dynamics",
168
- "settings": md_settings,
169
- "calc_settings": calc_settings,
170
- "folder_uuid": folder_uuid,
171
- "blocking": blocking,
172
- "ping_interval": ping_interval
173
- }
174
-
175
- # Add calc_engine at top level
176
- if engine:
177
- workflow_params["calc_engine"] = engine.lower()
178
-
179
- try:
180
- # Submit molecular dynamics calculation to Rowan
181
- result = rowan.compute(**workflow_params)
182
- return str(result)
183
- except Exception as e:
184
- error_response = {
185
- "success": False,
186
- "error": f"Molecular dynamics calculation failed: {str(e)}",
187
- "name": name,
188
- "molecule": molecule,
189
- "resolved_smiles": resolved_smiles
190
- }
191
- return str(error_response)
@@ -1,57 +0,0 @@
1
- from urllib.request import urlopen
2
- from urllib.parse import quote
3
-
4
-
5
- def CIRconvert(ids):
6
- """
7
- Convert molecule name/identifier to SMILES using Chemical Identifier Resolver.
8
-
9
- Args:
10
- ids (str): Molecule name or identifier (e.g., 'Aspirin', '3-Methylheptane', CAS numbers)
11
-
12
- Returns:
13
- str: SMILES string if found, 'Did not work' if failed
14
- """
15
- try:
16
- url = 'http://cactus.nci.nih.gov/chemical/structure/' + quote(ids) + '/smiles'
17
- ans = urlopen(url).read().decode('utf8')
18
- return ans
19
- except:
20
- return 'Did not work'
21
-
22
-
23
- def rowan_molecule_lookup(molecule_name: str) -> str:
24
- """
25
- Convert a molecule name to SMILES using Chemical Identifier Resolver.
26
-
27
- Args:
28
- molecule_name (str): Name of the molecule (e.g., 'aspirin', 'benzene')
29
-
30
- Returns:
31
- str: SMILES notation, or error message if not found
32
- """
33
- smiles = CIRconvert(molecule_name)
34
-
35
- if smiles == 'Did not work':
36
- return f"{molecule_name}: Not found"
37
- else:
38
- return smiles.strip() # Remove any trailing newlines
39
-
40
-
41
- def batch_convert(identifiers):
42
- """
43
- Convert multiple molecule identifiers to SMILES.
44
-
45
- Args:
46
- identifiers (list): List of molecule names/identifiers
47
-
48
- Returns:
49
- dict: Dictionary mapping identifiers to SMILES
50
- """
51
- results = {}
52
-
53
- for ids in identifiers:
54
- results[ids] = CIRconvert(ids)
55
-
56
- return results
57
-
@@ -1,171 +0,0 @@
1
- """
2
- Rowan multistage optimization function for geometry optimization.
3
- """
4
-
5
- import os
6
- import logging
7
- import time
8
- from typing import Optional, List, Dict, Any
9
-
10
- try:
11
- import rowan
12
- except ImportError:
13
- rowan = None
14
-
15
- # Setup logging
16
- logger = logging.getLogger(__name__)
17
-
18
- # Setup API key
19
- api_key = os.getenv("ROWAN_API_KEY")
20
- if rowan and api_key:
21
- rowan.api_key = api_key
22
-
23
- def log_rowan_api_call(workflow_type: str, **kwargs):
24
- """Log Rowan API calls with detailed parameters."""
25
-
26
- # Special handling for long-running calculations
27
- if workflow_type in ["multistage_opt", "conformer_search"]:
28
- ping_interval = kwargs.get('ping_interval', 5)
29
- blocking = kwargs.get('blocking', True)
30
- if blocking:
31
- if workflow_type == "multistage_opt":
32
- logger.info(f" Multi-stage optimization may take several minutes...")
33
- else:
34
- logger.info(f" Conformer search may take several minutes...")
35
- logger.info(f" Progress will be checked every {ping_interval} seconds")
36
- else:
37
- logger.info(f" {workflow_type.replace('_', ' ').title()} submitted without waiting")
38
-
39
- try:
40
- start_time = time.time()
41
- result = rowan.compute(workflow_type=workflow_type, **kwargs)
42
- api_time = time.time() - start_time
43
-
44
- if isinstance(result, dict) and 'uuid' in result:
45
- job_status = result.get('status', result.get('object_status', 'Unknown'))
46
- status_names = {0: "Queued", 1: "Running", 2: "Completed", 3: "Failed", 4: "Stopped", 5: "Awaiting Queue"}
47
- status_text = status_names.get(job_status, f"Unknown ({job_status})")
48
-
49
- return result
50
-
51
- except Exception as e:
52
- api_time = time.time() - start_time
53
- raise e
54
-
55
- def rowan_multistage_opt(
56
- name: str,
57
- molecule: str,
58
- mode: str = "rapid",
59
- solvent: Optional[str] = None,
60
- xtb_preopt: bool = False,
61
- constraints: Optional[List[Dict[str, Any]]] = None,
62
- transition_state: bool = False,
63
- frequencies: bool = False,
64
- folder_uuid: Optional[str] = None,
65
- blocking: bool = True,
66
- ping_interval: int = 30
67
- ) -> str:
68
- """Run multi-level geometry optimization using stjames MultiStageOptWorkflow.
69
-
70
- Performs hierarchical optimization using multiple levels of theory based on mode. There are only four valid modes:
71
-
72
- **RAPID** (default): GFN2-xTB optimization → r²SCAN-3c single point
73
- **RECKLESS**: GFN-FF optimization → GFN2-xTB single point
74
- **CAREFUL**: GFN2-xTB preopt → r²SCAN-3c optimization → ωB97X-3c single point
75
- **METICULOUS**: GFN2-xTB preopt → r²SCAN-3c opt → ωB97X-3c opt → ωB97M-D3BJ/def2-TZVPPD single point
76
-
77
- This follows the exact stjames MultiStageOptWorkflow implementation for:
78
- - High accuracy final structures
79
- - Efficient computational cost
80
- - Reliable convergence across chemical space
81
-
82
- Args:
83
- name: Name for the calculation
84
- molecule: Molecule SMILES string
85
- mode: Optimization mode - "rapid", "reckless", "careful", or "meticulous" (default: "rapid")
86
- solvent: Solvent for single point calculation (e.g., "water", "dmso", "acetone")
87
- xtb_preopt: Whether to include xTB pre-optimization step (default: False)
88
- constraints: List of optimization constraints (default: None)
89
- transition_state: Whether this is a transition state optimization (default: False)
90
- frequencies: Whether to calculate vibrational frequencies (default: False)
91
- folder_uuid: Optional folder UUID for organization
92
- blocking: Whether to wait for completion (default: True)
93
- ping_interval: Check status interval in seconds (default: 30)
94
-
95
- Returns:
96
- Comprehensive optimization results following MultiStageOptWorkflow format
97
-
98
- Example:
99
- # Basic optimization
100
- result = rowan_multistage_opt("aspirin_opt", "CC(=O)Oc1ccccc1C(=O)O")
101
-
102
- # With solvent and frequency analysis
103
- result = rowan_multistage_opt(
104
- "aspirin_water",
105
- "CC(=O)Oc1ccccc1C(=O)O",
106
- mode="careful",
107
- solvent="water",
108
- frequencies=True
109
- )
110
- """
111
-
112
- # Validate mode parameter - only allow Rowan's supported modes
113
- valid_modes = ["rapid", "reckless", "careful", "meticulous"]
114
- if mode.lower() not in valid_modes:
115
- error_msg = f"Invalid mode '{mode}'. Must be one of: {', '.join(valid_modes)}"
116
- logger.error(f"Mode validation failed: {error_msg}")
117
- return f"Error: {error_msg}"
118
-
119
- # Prepare parameters following stjames MultiStageOptWorkflow structure
120
- params = {
121
- "name": name,
122
- "molecule": molecule,
123
- "initial_molecule": molecule, # Required by MultiStageOptWorkflow
124
- "mode": mode.lower(),
125
- "folder_uuid": folder_uuid,
126
- "blocking": blocking,
127
- "ping_interval": ping_interval
128
- }
129
-
130
- # Add optional parameters if specified
131
- if solvent:
132
- params["solvent"] = solvent
133
-
134
- if xtb_preopt:
135
- params["xtb_preopt"] = xtb_preopt
136
-
137
- if constraints:
138
- params["constraints"] = constraints
139
-
140
- if transition_state:
141
- params["transition_state"] = transition_state
142
-
143
- if frequencies:
144
- params["frequencies"] = frequencies
145
-
146
- # Submit to Rowan using multistage_opt workflow
147
- result = log_rowan_api_call(
148
- workflow_type="multistage_opt",
149
- **params
150
- )
151
- return str(result)
152
-
153
- def test_rowan_multistage_opt():
154
- """Test the rowan_multistage_opt function with stjames parameters."""
155
- try:
156
- # Test with stjames-compatible parameters
157
- result = rowan_multistage_opt(
158
- name="test_stjames_opt",
159
- molecule="CCO",
160
- mode="rapid",
161
- blocking=False
162
- )
163
- print("✅ Multistage optimization test successful!")
164
- print(f"Result length: {len(result)} characters")
165
- return True
166
- except Exception as e:
167
- print(f"Multistage optimization test failed: {e}")
168
- return False
169
-
170
- if __name__ == "__main__":
171
- test_rowan_multistage_opt()
@@ -1,200 +0,0 @@
1
- """
2
- PDB handler for Rowan docking - attempts to handle PDB input intelligently
3
- """
4
-
5
- import os
6
- import logging
7
- import requests
8
- from typing import Optional, Dict, Any, Tuple
9
- import rowan
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
- def parse_pdb_content(pdb_content: str) -> Dict[str, Any]:
14
- """
15
- Parse PDB content into a structured format that might work with Rowan.
16
-
17
- This attempts to extract key information from PDB files that the
18
- Rowan API might accept.
19
- """
20
- lines = pdb_content.split('\n')
21
-
22
- # Extract basic information
23
- header = ""
24
- title = ""
25
- atoms = []
26
-
27
- for line in lines:
28
- if line.startswith("HEADER"):
29
- header = line[10:50].strip()
30
- elif line.startswith("TITLE"):
31
- title += line[10:].strip() + " "
32
- elif line.startswith("ATOM") or line.startswith("HETATM"):
33
- atoms.append(line)
34
-
35
- # Try to create a structure that might work with Rowan's PDB format
36
- pdb_data = {
37
- "description": {
38
- "title": title.strip() or header,
39
- "classification": header
40
- },
41
- "experiment": {
42
- "technique": "X-RAY DIFFRACTION" # Default assumption
43
- },
44
- "geometry": {},
45
- "contents": pdb_content, # Keep full content
46
- "name": f"pdb_upload_{len(pdb_content)}"
47
- }
48
-
49
- return pdb_data
50
-
51
- def attempt_pdb_upload(pdb_id: str, pdb_content: str, folder_uuid: Optional[str] = None) -> Tuple[Optional[str], str]:
52
- """
53
- Attempt to upload PDB content to Rowan.
54
-
55
- Returns:
56
- Tuple of (uuid or None, status message)
57
- """
58
- try:
59
- # First, try the Protein.create method if it exists
60
- if hasattr(rowan, 'Protein') and hasattr(rowan.Protein, 'create'):
61
- logger.info(f"Attempting to create protein from PDB {pdb_id}")
62
- result = rowan.Protein.create(
63
- name=f"{pdb_id}_upload",
64
- pdb_content=pdb_content,
65
- folder_uuid=folder_uuid
66
- )
67
- if result and 'uuid' in result:
68
- return result['uuid'], f"Successfully uploaded PDB {pdb_id}"
69
-
70
- # Try alternative upload methods
71
- if hasattr(rowan, 'upload_pdb'):
72
- result = rowan.upload_pdb(pdb_content, name=pdb_id)
73
- if result and 'uuid' in result:
74
- return result['uuid'], f"Successfully uploaded PDB {pdb_id}"
75
-
76
- # Try to create a file upload
77
- if hasattr(rowan, 'File') and hasattr(rowan.File, 'create'):
78
- result = rowan.File.create(
79
- name=f"{pdb_id}.pdb",
80
- content=pdb_content,
81
- file_type="pdb",
82
- folder_uuid=folder_uuid
83
- )
84
- if result and 'uuid' in result:
85
- return result['uuid'], f"Uploaded PDB {pdb_id} as file"
86
-
87
- except Exception as e:
88
- logger.error(f"Failed to upload PDB {pdb_id}: {str(e)}")
89
-
90
- return None, f"Cannot upload PDB through API - manual upload required"
91
-
92
- def handle_pdb_input(pdb_input: str, folder_uuid: Optional[str] = None) -> Dict[str, Any]:
93
- """
94
- Handle PDB input (either PDB ID or UUID) and return information for docking.
95
-
96
- Args:
97
- pdb_input: Either a PDB ID (like "4Z18") or a UUID
98
- folder_uuid: Optional folder for uploads
99
-
100
- Returns:
101
- Dictionary with:
102
- - success: bool
103
- - uuid: str (if successful)
104
- - message: str (status/error message)
105
- - pdb_id: str (if it was a PDB ID)
106
- - instructions: str (what to do next)
107
- """
108
- # Check if it's a UUID (36 chars with dashes)
109
- if len(pdb_input) == 36 and pdb_input.count('-') == 4:
110
- return {
111
- "success": True,
112
- "uuid": pdb_input,
113
- "message": "Using provided protein UUID",
114
- "instructions": None
115
- }
116
-
117
- # Assume it's a PDB ID
118
- pdb_id = pdb_input.upper()
119
-
120
- # Fetch PDB content
121
- try:
122
- url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
123
- response = requests.get(url, timeout=30)
124
- response.raise_for_status()
125
- pdb_content = response.text
126
- logger.info(f"Successfully fetched PDB {pdb_id} ({len(pdb_content)} characters)")
127
- except Exception as e:
128
- return {
129
- "success": False,
130
- "uuid": None,
131
- "message": f"Failed to fetch PDB {pdb_id}: {str(e)}",
132
- "pdb_id": pdb_id,
133
- "instructions": "Please verify the PDB ID is correct"
134
- }
135
-
136
- # Attempt to upload
137
- uuid, upload_status = attempt_pdb_upload(pdb_id, pdb_content, folder_uuid)
138
-
139
- if uuid:
140
- return {
141
- "success": True,
142
- "uuid": uuid,
143
- "message": upload_status,
144
- "pdb_id": pdb_id,
145
- "instructions": None
146
- }
147
-
148
- # Upload failed - provide instructions
149
- instructions = f"""
150
- To use PDB {pdb_id} for docking:
151
-
152
- 1. Download the PDB file:
153
- https://files.rcsb.org/download/{pdb_id}.pdb
154
-
155
- 2. Upload to your Rowan account:
156
- https://labs.rowansci.com
157
-
158
- 3. Get the protein UUID from your account
159
-
160
- 4. Use the UUID in your docking call:
161
- rowan_docking(
162
- target_uuid="your-protein-uuid-here",
163
- ...
164
- )
165
- """
166
-
167
- return {
168
- "success": False,
169
- "uuid": None,
170
- "message": upload_status,
171
- "pdb_id": pdb_id,
172
- "instructions": instructions
173
- }
174
-
175
- def enhance_docking_with_pdb_handler(
176
- target_input: Optional[str] = None,
177
- target_uuid: Optional[str] = None,
178
- **kwargs
179
- ) -> Tuple[Optional[str], str]:
180
- """
181
- Enhanced target handling for docking that accepts PDB IDs.
182
-
183
- Returns:
184
- Tuple of (target_uuid or None, message)
185
- """
186
- # If UUID already provided, use it
187
- if target_uuid:
188
- return target_uuid, "Using provided target UUID"
189
-
190
- # If target_input provided, try to handle it
191
- if target_input:
192
- result = handle_pdb_input(target_input, kwargs.get('folder_uuid'))
193
-
194
- if result['success']:
195
- return result['uuid'], result['message']
196
- else:
197
- # Return None but with helpful message
198
- return None, result['instructions'] or result['message']
199
-
200
- return None, "No protein target specified"