import re import httpx from typing import Any from app.tools.base import BaseTool from app.config import settings from app.services.cache import ttl_cache class UniprotTool(BaseTool): name = "uniprot" @ttl_cache(ttl=86400, prefix="uniprot") async def run(self, input: dict) -> dict: accession = input.get("accession", "").strip().upper() if not accession: return {"error": "No accession provided"} data = await self._fetch(accession) if "error" in data: return data return { "accession": data.get("primaryAccession", ""), "full_name": self._extract_name(data), "ec_number": (data.get("proteinDescription", {}) or {}).get("ecNumbers", [{}])[0].get("ecNumber", "") if data.get("proteinDescription") else "", "gene_names": [g.get("geneName", {}).get("value", "") for g in (data.get("genes") or []) if g.get("geneName")], "organism": ((data.get("organism", {}) or {}).get("scientificName", "")), "functions": self._extract_functions(data), "keywords": [kw.get("name", "") for kw in (data.get("keywords") or [])], "sequence": (data.get("sequence", {}) or {}).get("value", ""), "sequence_length": ((data.get("sequence", {}) or {}).get("length", 0)), "subcellular_locations": self._extract_locations(data), "pdb_ids": self._extract_pdb(data), "features": self._extract_features(data), "go_terms": self._extract_go_terms(data), "cds_accessions": self._extract_cds_accessions(data), } async def _fetch(self, accession: str) -> dict: accession = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', accession) url = f"{settings.UNIPROT_BASE_URL}/{accession}" async with httpx.AsyncClient(timeout=15) as client: resp = await client.get(url, params={"format": "json"}) if resp.status_code == 404: return {"error": f"Accession {accession} not found"} if resp.status_code >= 400: return {"error": f"UniProt returned {resp.status_code} for {accession}"} return resp.json() def _extract_name(self, data: dict) -> str: desc = data.get("proteinDescription", {}) or {} rec_name = desc.get("recommendedName", {}) or {} return (rec_name.get("fullName", {}) or {}).get("value", "") def _extract_functions(self, data: dict) -> list[str]: comments = data.get("comments") or [] funcs = [] for c in comments: if c.get("commentType") == "FUNCTION": texts = c.get("texts") or [] for t in texts: val = (t.get("value") or "").strip() if val: funcs.append(val) return funcs def _extract_locations(self, data: dict) -> list[str]: comments = data.get("comments") or [] locs = [] for c in comments: if c.get("commentType") == "SUBCELLULAR_LOCATION": subcels = c.get("subcellularLocations") or [] for s in subcels: loc = (s.get("location", {}) or {}).get("value", "") if loc: locs.append(loc) return locs def _extract_cds_accessions(self, data: dict) -> list[dict]: refs = data.get("uniProtKBCrossReferences") or [] cds = [] seen_ids = set() for r in refs: db = r.get("database", "") if db in ("EMBL", "GenBank", "DDBJ"): props = {p.get("key", ""): p.get("value", "") for p in (r.get("properties") or [])} acc = r.get("id", "") if acc and acc not in seen_ids: seen_ids.add(acc) cds.append({ "database": db, "accession": acc, "protein_sequence_id": props.get("protein sequence ID", ""), "nucleotide_sequence_id": props.get("nucleotide sequence ID", ""), }) return cds def _extract_pdb(self, data: dict) -> list[str]: refs = data.get("uniProtKBCrossReferences") or [] pdbs = [] for r in refs: if r.get("database") == "PDB": pdbs.append(r.get("id", "")) return pdbs def _extract_features(self, data: dict) -> list[dict]: features = data.get("features") or [] result = [] for f in features: result.append({ "type": f.get("type", ""), "description": f.get("description", ""), "begin": (f.get("location", {}) or {}).get("start", {}).get("value"), "end": (f.get("location", {}) or {}).get("end", {}).get("value"), }) return result def _extract_go_terms(self, data: dict) -> list[str]: refs = data.get("uniProtKBCrossReferences") or [] go = [] for r in refs: if r.get("database") == "GO": term = r.get("properties", [{}])[0].get("value", "") if r.get("properties") else "" if term: go.append(term) return go