Spaces:
Sleeping
Sleeping
File size: 5,538 Bytes
ba4ad33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | import os
import requests
from typing import Optional, Dict, List
from rdkit import Chem
from rdkit.Chem import Descriptors, rdMolDescriptors
"""
LLM-based Generation Layer for Chemical Similarity Explanation
Version 2.0: Optimized with RDKit Grounding & Hallucination Guardrails
"""
# 1. RDKit Metadata Engine
def get_rdkit_metadata(smiles: str) -> Dict:
"""Extracts factual chemical data to anchor the LLM response."""
try:
mol = Chem.MolFromSmiles(smiles)
if not mol:
return {}
return {
"formula": rdMolDescriptors.CalcMolFormula(mol),
"mw": round(Descriptors.MolWt(mol), 2),
"heavy_atoms": mol.GetNumHeavyAtoms(),
"formal_charge": Chem.GetFormalCharge(mol)
}
except Exception:
return {}
# 2. System Prompt
def build_system_prompt() -> str:
"""Build the system prompt with strict chemical constraints."""
return """Role:
You are an expert Cheminformatics AI Assistant. Your task is to explain the structural similarity between a "Query Compound" and a "Match Compound" based on SMILES notation and provided RDKit metadata.
Strict Guidelines to Prevent Hallucinations:
1. GROUNDING: Use the provided Molecular Formula to verify atom counts. If the formula is C4H9Br, do NOT say it has 4 Bromine atoms.
2. TETRAVALENCY: Carbon is ALWAYS tetravalent (4 bonds). Never describe a carbon atom as "pentavalent" or having more than 4 bonds.
3. SMILES PARSING: Parentheses ( ) denote branches on the same atom. E.g., C(Br)(Br) means two Bromines on the same Carbon.
4. NO GUESSING NAMES: Do not attempt IUPAC naming unless certain. Use descriptive terms like "tri-halogenated methane" or "tert-butyl scaffold".
5. ISOSTERES: Identify isosteric replacements correctly (e.g., swapping Br for Cl or F).
6. CONCISENESS: Keep the explanation to 2-3 precise technical sentences."""
# 3. User Prompt
def build_user_prompt(query_smiles: str, match_smiles: str, similarity: float) -> str:
"""Combines SMILES with RDKit metadata for the LLM context."""
q_meta = get_rdkit_metadata(query_smiles)
m_meta = get_rdkit_metadata(match_smiles)
return f"""Analyze the following compound pair:
Query: {query_smiles}
- Formula: {q_meta.get('formula', 'N/A')}
- Heavy Atoms: {q_meta.get('heavy_atoms', 'N/A')}
Match: {match_smiles}
- Formula: {m_meta.get('formula', 'N/A')}
- Heavy Atoms: {m_meta.get('heavy_atoms', 'N/A')}
Similarity Score: {similarity:.3f}
Task: Explain why these compounds are structurally similar. Focus on the core scaffold and atom substitutions. Reference the formulas to ensure accuracy."""
# 4. Generation Core
def generate_explanation(
query_smiles: str,
compound_smiles: str,
similarity_score: float
) -> Optional[str]:
"""Generates a fact-checked explanation using Llama-3.1 via HF Router."""
try:
api_key = os.environ.get("HF_TOKEN")
if not api_key:
return _generate_fallback_explanation(similarity_score)
system_prompt = build_system_prompt()
user_prompt = build_user_prompt(query_smiles, compound_smiles, similarity_score)
# HuggingFace Router API
api_url = "https://router.huggingface.co/v1/chat/completions"
headers = {"Authorization": f"Bearer {api_key}"}
payload = {
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
"model": "meta-llama/Llama-3.1-8B-Instruct:fastest",
"temperature": 0.1, # Low temperature for high factual precision
"max_tokens": 100
}
response = requests.post(api_url, headers=headers, json=payload, timeout=10)
response.raise_for_status()
result = response.json()
explanation = result["choices"][0]["message"]["content"].strip()
return explanation if len(explanation) > 10 else _generate_fallback_explanation(similarity_score)
except Exception:
return _generate_fallback_explanation(similarity_score)
def _generate_fallback_explanation(similarity_score: float) -> str:
"""Heuristic fallback in case of API failure."""
if similarity_score >= 0.95:
return "Extremely high structural similarity - compounds share an identical core scaffold with minimal substituent variation."
return "Significant structural similarity based on shared molecular framework and functional group distribution."
# 5. Batch Processing
def generate_explanations_batch(query_smiles: str, search_results: List[Dict]) -> List[Dict]:
"""Adds an 'explanation' field to each search result."""
for result in search_results:
result["explanation"] = generate_explanation(
query_smiles,
result["smiles"],
result["similarity_score"]
)
return search_results
# Example Usage:
if __name__ == "__main__":
# Mock search results
query = "C(Br)(Br)(Br)Br"
results = [
{"smiles": "C(Cl)(Cl)(Cl)Br", "similarity_score": 0.998},
{"smiles": "CC(C)(C)Br", "similarity_score": 0.998}
]
# You need to set your HF_TOKEN in environment variables
# os.environ["HF_TOKEN"] = "your_token_here"
updated_results = generate_explanations_batch(query, results)
for res in updated_results:
print(f"SMILES: {res['smiles']}\nScore: {res['similarity_score']}\nExplanation: {res['explanation']}\n{'-'*30}") |