rowan-mcp 1.0.0__py3-none-any.whl → 1.0.2__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.

rowan_mcp/__init__.py CHANGED
@@ -6,7 +6,7 @@ for integrating with Rowan's computational chemistry platform.
6
6
  """
7
7
 
8
8
  __version__ = "1.0.0"
9
- __author__ = "Rowan MCP Team"
9
+ __author__ = "Kat Yenko"
10
10
  __description__ = "MCP server for Rowan computational chemistry platform"
11
11
 
12
12
  from .server import main
@@ -42,8 +42,8 @@ def rowan_conformers(
42
42
  folder_uuid: Optional[str] = None,
43
43
  blocking: bool = True,
44
44
  ping_interval: int = 5
45
- ) -> str:
46
- """Generate and optimize molecular conformers using Rowan's conformer_search workflow.
45
+ ):
46
+ """Generate and optimize molecular conformers using Rowan's conformer_search workflow. Valid modes are "reckless", "rapid", "careful", and "meticulous", and default to using SMILES strings for the "molecule" parameter.
47
47
 
48
48
  Args:
49
49
  name: Name for the calculation
@@ -65,7 +65,7 @@ def rowan_conformers(
65
65
  f"Invalid mode '{mode}'. Valid modes are: {', '.join(valid_modes)}"
66
66
  )
67
67
 
68
- result = log_rowan_api_call(
68
+ return log_rowan_api_call(
69
69
  workflow_type="conformer_search",
70
70
  name=name,
71
71
  molecule=molecule,
@@ -75,61 +75,6 @@ def rowan_conformers(
75
75
  blocking=blocking,
76
76
  ping_interval=ping_interval
77
77
  )
78
-
79
- # Format results based on whether we waited or not
80
- if blocking:
81
- # We waited for completion - format actual results
82
- status = result.get('status', result.get('object_status', 'Unknown'))
83
-
84
- if status == 2: # Completed successfully
85
- formatted = f" Conformer search for '{name}' completed successfully!\n\n"
86
- elif status == 3: # Failed
87
- formatted = f" Conformer search for '{name}' failed!\n\n"
88
- else:
89
- formatted = f" Conformer search for '{name}' finished with status {status}\n\n"
90
-
91
- formatted += f" Molecule: {molecule}\n"
92
- formatted += f" Job UUID: {result.get('uuid', 'N/A')}\n"
93
- formatted += f" Status: {status}\n"
94
- formatted += f" Mode: {mode.upper()}\n"
95
- formatted += f" Max Conformers: {max_conformers}\n"
96
-
97
- # Try to extract actual results
98
- if isinstance(result, dict) and 'object_data' in result and result['object_data']:
99
- data = result['object_data']
100
-
101
- # Count conformers found
102
- if 'conformers' in data:
103
- conformer_count = len(data['conformers']) if isinstance(data['conformers'], list) else data.get('num_conformers', 'Unknown')
104
- formatted += f" Generated Conformers: {conformer_count}\n"
105
-
106
- # Energy information
107
- if 'energies' in data and isinstance(data['energies'], list) and data['energies']:
108
- energies = data['energies']
109
- min_energy = min(energies)
110
- max_energy = max(energies)
111
- energy_range = max_energy - min_energy
112
- formatted += f" Energy Range: {min_energy:.3f} to {max_energy:.3f} kcal/mol (Δ={energy_range:.3f})\n"
113
- formatted += f" Lowest Energy Conformer: {min_energy:.3f} kcal/mol\n"
114
-
115
- # Additional properties if available
116
- if 'properties' in data:
117
- props = data['properties']
118
- formatted += f" Properties calculated: {', '.join(props.keys())}\n"
119
-
120
- # Basic guidance
121
- if status == 2:
122
- formatted += f"\n Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for detailed data\n"
123
- else:
124
- # Non-blocking mode - just submission confirmation
125
- formatted = f" Conformer search for '{name}' submitted!\n\n"
126
- formatted += f" Molecule: {molecule}\n"
127
- formatted += f" Job UUID: {result.get('uuid', 'N/A')}\n"
128
- formatted += f" Status: {result.get('status', 'Submitted')}\n"
129
- formatted += f" Mode: {mode.upper()}\n"
130
- formatted += f" Max Conformers: {max_conformers}\n"
131
-
132
- return formatted
133
78
 
134
79
  if __name__ == "__main__":
135
80
  pass
@@ -186,68 +186,10 @@ def rowan_electronic_properties(
186
186
  **electronic_params
187
187
  )
188
188
 
189
- # Enhanced result formatting for electronic properties
190
- if blocking:
191
- status = result.get('status', result.get('object_status', 'Unknown'))
192
-
193
- if status == 2: # Completed successfully
194
- formatted = f"Electronic properties calculation for '{name}' completed successfully!\n\n"
195
- elif status == 3: # Failed
196
- formatted = f"Electronic properties calculation for '{name}' failed!\n\n"
197
- else:
198
- formatted = f"Electronic properties calculation for '{name}' submitted!\n\n"
199
-
200
- formatted += f"Molecule: {molecule}\n"
201
- formatted += f"Canonical SMILES: {canonical_smiles}\n"
202
- formatted += f"Job UUID: {result.get('uuid', 'N/A')}\n"
203
- formatted += f"Status: {status}\n\n"
204
-
205
- formatted += f"Molecule Lookup: Advanced PubChemPy + SQLite + RDKit system\n\n"
206
- formatted += f"Calculation Settings:\n"
207
- formatted += f"• Method: {method.upper()}\n"
208
- formatted += f"• Basis Set: {basis_set}\n"
209
- formatted += f"• Engine: {engine.upper()}\n"
210
- formatted += f"• Charge: {charge}, Multiplicity: {multiplicity}\n\n"
211
-
212
- formatted += f"Property Calculations:\n"
213
- formatted += f"• Density Cube: {'Enabled' if compute_density_cube else 'Disabled'}\n"
214
- formatted += f"• ESP Cube: {'Enabled' if compute_electrostatic_potential_cube else 'Disabled'}\n"
215
- formatted += f"• Occupied MOs: {compute_num_occupied_orbitals}\n"
216
- formatted += f"• Virtual MOs: {compute_num_virtual_orbitals}\n\n"
217
-
218
- if status == 2:
219
- formatted += f"Additional Analysis:\n"
220
- formatted += f"• Use rowan_calculation_retrieve('{result.get('uuid')}') for full calculation details\n"
221
- formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for workflow metadata\n"
222
-
223
- elif status == 3:
224
- formatted += f"Troubleshooting:\n"
225
- formatted += f"• Try simpler method/basis: method='hf', basis_set='sto-3g'\n"
226
- formatted += f"• Check molecular charge and multiplicity\n"
227
- formatted += f"• Disable cube generation for faster calculations\n"
228
- formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for error details\n"
229
- else:
230
- formatted += f"Next Steps:\n"
231
- formatted += f"• Monitor status with rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}')\n"
232
- formatted += f"• Electronic properties calculations may take several minutes\n"
233
-
234
- return formatted
235
- else:
236
- return str(result)
189
+ return result
237
190
 
238
191
  except Exception as e:
239
- error_msg = f"Electronic properties calculation failed: {str(e)}\n\n"
240
- error_msg += f"Molecule: {molecule}\n"
241
- error_msg += f"Canonical SMILES: {canonical_smiles}\n"
242
- error_msg += f"Settings: {method}/{basis_set}/{engine}\n\n"
243
- error_msg += f"Molecule Lookup: Advanced PubChemPy + SQLite + RDKit system\n\n"
244
- error_msg += f"Common Issues:\n"
245
- error_msg += f"• Invalid method/basis set combination\n"
246
- error_msg += f"• Incorrect charge/multiplicity for molecule\n"
247
- error_msg += f"• Engine compatibility issues\n"
248
- error_msg += f"• Molecule not found in PubChem database\n"
249
- error_msg += f"• Try with default parameters first\n"
250
- return error_msg
192
+ return f"Electronic properties calculation failed: {str(e)}"
251
193
 
252
194
  def test_electronic_properties():
253
195
  """Test the electronic properties function with advanced molecule lookup."""
@@ -54,53 +54,33 @@ def log_rowan_api_call(workflow_type: str, **kwargs):
54
54
  raise e
55
55
 
56
56
  def lookup_molecule_smiles(molecule_name: str) -> str:
57
- """Look up canonical SMILES for common molecule names."""
58
- # Common molecule SMILES database
59
- MOLECULE_SMILES = {
60
- # Aromatics
61
- "phenol": "Oc1ccccc1",
62
- "benzene": "c1ccccc1",
63
- "toluene": "Cc1ccccc1",
64
- "aniline": "Nc1ccccc1",
65
- "benzoic acid": "O=C(O)c1ccccc1",
66
- "salicylic acid": "O=C(O)c1ccccc1O",
67
- "aspirin": "CC(=O)Oc1ccccc1C(=O)O",
57
+ """Look up canonical SMILES using the advanced molecule_lookup system.
58
+
59
+ Uses PubChemPy + SQLite caching + RDKit validation for scalable molecule lookup.
60
+ """
61
+ try:
62
+ # Import the advanced molecule lookup system
63
+ from .molecule_lookup import get_lookup_instance
68
64
 
69
- # Solvents
70
- "water": "O",
71
- "acetone": "CC(=O)C",
72
- "dmso": "CS(=O)C",
73
- "dmf": "CN(C)C=O",
74
- "thf": "C1CCOC1",
75
- "dioxane": "C1COCCO1",
76
- "chloroform": "ClC(Cl)Cl",
77
- "dichloromethane": "ClCCl",
65
+ lookup = get_lookup_instance()
66
+ smiles, source, metadata = lookup.get_smiles(molecule_name)
78
67
 
79
- # Others
80
- "glucose": "OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O",
81
- "ethylene": "C=C",
82
- "acetylene": "C#C",
83
- "formaldehyde": "C=O",
84
- "ammonia": "N",
85
- "hydrogen peroxide": "OO",
86
- "carbon dioxide": "O=C=O",
87
- }
88
-
89
- # Normalize the input (lowercase, strip whitespace)
90
- normalized_name = molecule_name.lower().strip()
91
-
92
- # Direct lookup
93
- if normalized_name in MOLECULE_SMILES:
94
- return MOLECULE_SMILES[normalized_name]
95
-
96
- # Try partial matches for common variations
97
- for name, smiles in MOLECULE_SMILES.items():
98
- if normalized_name in name or name in normalized_name:
99
- logger.info(f"SMILES Lookup (partial match): '{molecule_name}' → '{name}' → '{smiles}'")
68
+ if smiles:
69
+ logger.info(f"Molecule lookup successful: '{molecule_name}' → '{smiles}' (source: {source})")
100
70
  return smiles
101
-
102
- # If no match found, return the original input (assume it's already SMILES)
103
- return molecule_name
71
+ else:
72
+ logger.warning(f"Molecule lookup failed for '{molecule_name}': {metadata.get('error', 'Unknown error')}")
73
+ # Return original input as fallback (might be valid SMILES)
74
+ return molecule_name
75
+
76
+ except ImportError as e:
77
+ logger.error(f"Could not import molecule_lookup: {e}")
78
+ # Fallback: return original input
79
+ return molecule_name
80
+ except Exception as e:
81
+ logger.error(f"Molecule lookup error for '{molecule_name}': {e}")
82
+ # Fallback: return original input
83
+ return molecule_name
104
84
 
105
85
  def rowan_fukui(
106
86
  name: str,
@@ -135,9 +115,12 @@ def rowan_fukui(
135
115
  - Per-atom reactivity indices for site-specific analysis
136
116
  - Global reactivity descriptors
137
117
 
118
+ **Molecule Lookup**: Uses advanced PubChemPy + SQLite caching + RDKit validation system
119
+ for robust molecule identification and SMILES canonicalization.
120
+
138
121
  Args:
139
122
  name: Name for the calculation
140
- molecule: Molecule SMILES string or common name (e.g., "phenol", "benzene")
123
+ molecule: Molecule name (e.g., "aspirin", "taxol") or SMILES string
141
124
  optimize: Whether to optimize geometry before Fukui calculation (default: True)
142
125
  opt_method: Method for optimization (default: None, uses engine default)
143
126
  opt_basis_set: Basis set for optimization (default: None, uses engine default)
@@ -212,135 +195,16 @@ def rowan_fukui(
212
195
  if fukui_engine:
213
196
  fukui_params["fukui_engine"] = fukui_engine.lower()
214
197
 
215
- result = log_rowan_api_call(
216
- workflow_type="fukui",
217
- **fukui_params
218
- )
219
-
220
- if blocking:
221
- status = result.get('status', result.get('object_status', 'Unknown'))
222
-
223
- if status == 2: # Completed successfully
224
- formatted = f"Fukui analysis for '{name}' completed successfully!\n\n"
225
- elif status == 3: # Failed
226
- formatted = f"Fukui analysis for '{name}' failed!\n\n"
227
- else:
228
- formatted = f"Fukui analysis for '{name}' finished with status {status}\n\n"
229
-
230
- formatted += f"Molecule: {molecule}\n"
231
- formatted += f"SMILES: {canonical_smiles}\n"
232
- formatted += f"Job UUID: {result.get('uuid', 'N/A')}\n"
233
- formatted += f"Status: {status}\n"
234
-
235
- # Computational settings summary
236
- formatted += f"\nComputational Settings:\n"
237
- formatted += f" Optimization: {'Enabled' if optimize else 'Disabled'}\n"
238
- if optimize:
239
- opt_method_display = opt_settings.get('method', 'default') if opt_settings else 'default'
240
- formatted += f" Opt Method: {opt_method_display.upper()}\n"
241
- if opt_engine:
242
- formatted += f" Opt Engine: {opt_engine.upper()}\n"
243
- formatted += f" Fukui Method: {fukui_method.upper()}\n"
244
- if fukui_engine:
245
- formatted += f" Fukui Engine: {fukui_engine.upper()}\n"
246
- formatted += f" Charge: {charge}, Multiplicity: {multiplicity}\n"
198
+ try:
199
+ result = log_rowan_api_call(
200
+ workflow_type="fukui",
201
+ **fukui_params
202
+ )
247
203
 
248
- # Try to extract Fukui results
249
- if isinstance(result, dict) and 'object_data' in result and result['object_data']:
250
- data = result['object_data']
251
-
252
- # Global electrophilicity index
253
- if 'global_electrophilicity_index' in data and data['global_electrophilicity_index'] is not None:
254
- gei = data['global_electrophilicity_index']
255
- formatted += f"\nGlobal Electrophilicity Index: {gei:.4f}\n"
256
- if gei > 1.5:
257
- formatted += f" → Strong electrophile (highly reactive towards nucleophiles)\n"
258
- elif gei > 0.8:
259
- formatted += f" → Moderate electrophile\n"
260
- else:
261
- formatted += f" → Weak electrophile\n"
204
+ return result
262
205
 
263
- # Fukui indices per atom
264
- fukui_available = []
265
- if 'fukui_positive' in data and data['fukui_positive']:
266
- fukui_available.append("f(+)")
267
- if 'fukui_negative' in data and data['fukui_negative']:
268
- fukui_available.append("f(-)")
269
- if 'fukui_zero' in data and data['fukui_zero']:
270
- fukui_available.append("f(0)")
271
-
272
- if fukui_available:
273
- formatted += f"\nFukui Indices Available: {', '.join(fukui_available)}\n"
274
-
275
- # Analyze most reactive sites
276
- formatted += f"\nMost Reactive Sites:\n"
277
-
278
- # f(+) - electrophilic attack sites
279
- if 'fukui_positive' in data and data['fukui_positive']:
280
- f_plus = data['fukui_positive']
281
- if isinstance(f_plus, list) and len(f_plus) > 0:
282
- # Find top 3 sites
283
- indexed_values = [(i+1, val) for i, val in enumerate(f_plus) if val is not None]
284
- top_f_plus = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
285
- formatted += f" f(+) Top Sites (electrophilic attack): "
286
- formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_plus])}\n"
287
-
288
- # f(-) - nucleophilic attack sites
289
- if 'fukui_negative' in data and data['fukui_negative']:
290
- f_minus = data['fukui_negative']
291
- if isinstance(f_minus, list) and len(f_minus) > 0:
292
- indexed_values = [(i+1, val) for i, val in enumerate(f_minus) if val is not None]
293
- top_f_minus = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
294
- formatted += f" f(-) Top Sites (nucleophilic attack): "
295
- formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_minus])}\n"
296
-
297
- # f(0) - radical attack sites
298
- if 'fukui_zero' in data and data['fukui_zero']:
299
- f_zero = data['fukui_zero']
300
- if isinstance(f_zero, list) and len(f_zero) > 0:
301
- indexed_values = [(i+1, val) for i, val in enumerate(f_zero) if val is not None]
302
- top_f_zero = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
303
- formatted += f" f(0) Top Sites (radical attack): "
304
- formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_zero])}\n"
305
-
306
- # Status-specific guidance
307
- formatted += f"\nNext Steps:\n"
308
- if status == 2: # Completed
309
- formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for full per-atom data\n"
310
- formatted += f"• Higher Fukui values indicate more reactive sites\n"
311
- formatted += f"• f(+) predicts where nucleophiles will attack\n"
312
- formatted += f"• f(-) predicts where electrophiles will attack\n"
313
- formatted += f"• f(0) predicts radical reaction sites\n"
314
- elif status == 3: # Failed
315
- formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for error details\n"
316
- formatted += f"• Troubleshooting:\n"
317
- formatted += f" - Try disabling optimization: optimize=False\n"
318
- formatted += f" - Use faster Fukui method: fukui_method='gfn1_xtb'\n"
319
- formatted += f" - Check if molecule SMILES is valid\n"
320
- formatted += f" - Verify charge and multiplicity are correct\n"
321
- elif status in [0, 1, 5]: # Running
322
- formatted += f"• Check progress: rowan_workflow_management(action='status', workflow_uuid='{result.get('uuid')}')\n"
323
- if optimize:
324
- formatted += f"• Two-step process: optimization → Fukui calculation\n"
325
- formatted += f"• Fukui analysis may take 5-20 minutes depending on method and molecule size\n"
326
- elif status == 4: # Stopped
327
- formatted += f"• Check why stopped: rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}')\n"
328
- formatted += f"• You can restart with same or modified parameters\n"
329
- else: # Unknown
330
- formatted += f"• Check status: rowan_workflow_management(action='status', workflow_uuid='{result.get('uuid')}')\n"
331
-
332
- return formatted
333
- else:
334
- formatted = f"Fukui analysis for '{name}' submitted!\n\n"
335
- formatted += f"Molecule: {molecule}\n"
336
- formatted += f"SMILES: {canonical_smiles}\n"
337
- formatted += f"Job UUID: {result.get('uuid', 'N/A')}\n"
338
- formatted += f"Status: {result.get('status', 'Submitted')}\n"
339
- formatted += f"Optimization: {'Enabled' if optimize else 'Disabled'}\n"
340
- formatted += f"Fukui Method: {fukui_method.upper()}\n"
341
- formatted += f"Charge: {charge}, Multiplicity: {multiplicity}\n"
342
- formatted += f"\nUse rowan_workflow_management tools to check progress and retrieve results\n"
343
- return formatted
206
+ except Exception as e:
207
+ return f"Fukui analysis failed: {str(e)}"
344
208
 
345
209
  def test_fukui():
346
210
  """Test the fukui function."""
@@ -1,7 +1,6 @@
1
1
  """MacropKa workflow function for MCP server."""
2
2
 
3
3
  import os
4
- import json
5
4
  import logging
6
5
  from typing import Optional, Union, List
7
6
 
@@ -61,11 +60,11 @@ def rowan_macropka(
61
60
  try:
62
61
  # Validate pH range
63
62
  if min_pH >= max_pH:
64
- return json.dumps({"error": "min_pH must be less than max_pH"})
63
+ return "Error: min_pH must be less than max_pH"
65
64
 
66
65
  # Validate charge range
67
66
  if min_charge >= max_charge:
68
- return json.dumps({"error": "min_charge must be less than max_charge"})
67
+ return "Error: min_charge must be less than max_charge"
69
68
 
70
69
  # Log the API call
71
70
  log_rowan_api_call(
@@ -102,85 +101,11 @@ def rowan_macropka(
102
101
  compute_solvation_energy=compute_solvation_energy
103
102
  )
104
103
 
105
- if blocking:
106
- # Format completed results
107
- status = result.get("status", "unknown")
108
- uuid = result.get("uuid", "unknown")
109
-
110
- if status == "success":
111
- object_data = result.get("object_data", {})
112
-
113
- # Extract key results
114
- microstates = object_data.get("microstates", [])
115
- pka_values = object_data.get("pKa_values", [])
116
- isoelectric_point = object_data.get("isoelectric_point")
117
- solvation_energy = object_data.get("solvation_energy")
118
- kpuu_probability = object_data.get("kpuu_probability")
119
- microstate_weights_by_pH = object_data.get("microstate_weights_by_pH", [])
120
- logD_by_pH = object_data.get("logD_by_pH", [])
121
- aqueous_solubility_by_pH = object_data.get("aqueous_solubility_by_pH", [])
122
-
123
- formatted = f"✅ MacropKa calculation completed successfully!\n"
124
- formatted += f"🔖 Workflow UUID: {uuid}\n"
125
- formatted += f"📋 Status: {status}\n\n"
126
-
127
- # Format pKa values
128
- if pka_values:
129
- formatted += "📊 pKa Values:\n"
130
- for pka in pka_values:
131
- formatted += f" • {pka.get('initial_charge', 'N/A')} → {pka.get('final_charge', 'N/A')}: pKa = {pka.get('pKa', 'N/A')}\n"
132
- formatted += "\n"
133
-
134
- # Format microstates
135
- if microstates:
136
- formatted += f"🔬 Microstates ({len(microstates)} found):\n"
137
- for i, microstate in enumerate(microstates[:5]): # Show first 5
138
- formatted += f" {i+1}. Charge: {microstate.get('charge', 'N/A')}, Energy: {microstate.get('energy', 'N/A')} kcal/mol\n"
139
- if len(microstates) > 5:
140
- formatted += f" ... and {len(microstates) - 5} more\n"
141
- formatted += "\n"
142
-
143
- # Add other properties
144
- if isoelectric_point is not None:
145
- formatted += f"⚡ Isoelectric Point: pH {isoelectric_point}\n"
146
-
147
- if solvation_energy is not None:
148
- formatted += f"💧 Solvation Energy: {solvation_energy} kcal/mol\n"
149
-
150
- if kpuu_probability is not None:
151
- formatted += f"🧠 Kpuu Probability (≥0.3): {kpuu_probability:.2%}\n"
152
-
153
- # Show pH-dependent properties if available
154
- if logD_by_pH:
155
- formatted += f"\n📈 logD values available for {len(logD_by_pH)} pH points\n"
156
-
157
- if aqueous_solubility_by_pH:
158
- formatted += f"💧 Aqueous solubility values available for {len(aqueous_solubility_by_pH)} pH points\n"
159
-
160
- if microstate_weights_by_pH:
161
- formatted += f"⚖️ Microstate weights available for {len(microstate_weights_by_pH)} pH points\n"
162
-
163
- return formatted
164
- else:
165
- # Handle failed calculation
166
- return f"❌ MacropKa calculation failed\n🔖 UUID: {uuid}\n📋 Status: {status}\n💬 Check workflow details for more information"
167
- else:
168
- # Non-blocking mode - return submission confirmation
169
- uuid = result.get("uuid", "unknown")
170
- formatted = f"📋 MacropKa calculation submitted!\n"
171
- formatted += f"🔖 Workflow UUID: {uuid}\n"
172
- formatted += f"⏳ Status: Running...\n"
173
- formatted += f"💡 Use rowan_workflow_management to check status\n"
174
- formatted += f"\nCalculation parameters:\n"
175
- formatted += f" • pH range: {min_pH} - {max_pH}\n"
176
- formatted += f" • Charge range: {min_charge} to {max_charge}\n"
177
- formatted += f" • Compute solvation energy: {compute_solvation_energy}\n"
178
- formatted += f" • Compute aqueous solubility: {compute_aqueous_solubility}\n"
179
- return formatted
104
+ return result
180
105
 
181
106
  except Exception as e:
182
107
  logger.error(f"Error in rowan_macropka: {str(e)}")
183
- return json.dumps({"error": str(e)})
108
+ return f"MacropKa calculation failed: {str(e)}"
184
109
 
185
110
 
186
111
  # Test function