Spaces:
Sleeping
Sleeping
Update biopython/mcp_output/mcp_plugin/mcp_service.py
Browse files
biopython/mcp_output/mcp_plugin/mcp_service.py
CHANGED
|
@@ -36,6 +36,10 @@ mcp = FastMCP("biopython_gene_species_identification")
|
|
| 36 |
# Configure Entrez email (required by NCBI)
|
| 37 |
Entrez.email = os.getenv("BIOPYTHON_ENTREZ_EMAIL", "biopython-mcp@huggingface.co")
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
# ==============================================
|
| 41 |
# 基础工具 - Basic Utilities
|
|
@@ -141,7 +145,215 @@ def calculate_gc_content(payload: dict):
|
|
| 141 |
|
| 142 |
|
| 143 |
# ==============================================
|
| 144 |
-
# BLAST 搜索工具 - BLAST
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
# ==============================================
|
| 146 |
|
| 147 |
@mcp.tool(name="blast_search", description="Search NCBI database using BLAST to find similar sequences")
|
|
@@ -467,6 +679,174 @@ def predict_species(payload: dict):
|
|
| 467 |
# 完整流程工具 - Complete Workflow Tool
|
| 468 |
# ==============================================
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
@mcp.tool(name="identify_gene_species_complete",
|
| 471 |
description="Complete workflow: input unknown gene sequence, identify species through BLAST search and analysis")
|
| 472 |
def identify_gene_species_complete(payload: dict):
|
|
|
|
| 36 |
# Configure Entrez email (required by NCBI)
|
| 37 |
Entrez.email = os.getenv("BIOPYTHON_ENTREZ_EMAIL", "biopython-mcp@huggingface.co")
|
| 38 |
|
| 39 |
+
# Global storage for async BLAST jobs
|
| 40 |
+
blast_jobs: Dict[str, Dict[str, Any]] = {}
|
| 41 |
+
blast_lock = threading.Lock()
|
| 42 |
+
|
| 43 |
|
| 44 |
# ==============================================
|
| 45 |
# 基础工具 - Basic Utilities
|
|
|
|
| 145 |
|
| 146 |
|
| 147 |
# ==============================================
|
| 148 |
+
# 异步 BLAST 搜索工具 - Async BLAST Tools
|
| 149 |
+
# ==============================================
|
| 150 |
+
|
| 151 |
+
def _run_blast_async(job_id: str, sequence: str, database: str, program: str,
|
| 152 |
+
hitlist_size: int, expect: float):
|
| 153 |
+
"""后台运行 BLAST 搜索"""
|
| 154 |
+
try:
|
| 155 |
+
print(f"🔄 [Job {job_id}] Starting BLAST search in background...")
|
| 156 |
+
|
| 157 |
+
with blast_lock:
|
| 158 |
+
blast_jobs[job_id]["status"] = "running"
|
| 159 |
+
blast_jobs[job_id]["message"] = "BLAST search in progress..."
|
| 160 |
+
|
| 161 |
+
# 执行 BLAST
|
| 162 |
+
handle = NCBIWWW.qblast(
|
| 163 |
+
program=program,
|
| 164 |
+
database=database,
|
| 165 |
+
sequence=sequence,
|
| 166 |
+
hitlist_size=hitlist_size,
|
| 167 |
+
expect=expect,
|
| 168 |
+
format_type="XML"
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
blast_record = NCBIXML.read(handle)
|
| 172 |
+
handle.close()
|
| 173 |
+
|
| 174 |
+
# 处理结果
|
| 175 |
+
hits = []
|
| 176 |
+
for idx, alignment in enumerate(blast_record.alignments[:hitlist_size]):
|
| 177 |
+
if not alignment.hsps:
|
| 178 |
+
continue
|
| 179 |
+
|
| 180 |
+
hsp = alignment.hsps[0]
|
| 181 |
+
align_len = int(hsp.align_length)
|
| 182 |
+
identities = int(hsp.identities)
|
| 183 |
+
identity_pct = (identities / align_len * 100.0) if align_len else 0
|
| 184 |
+
|
| 185 |
+
hits.append({
|
| 186 |
+
"rank": idx + 1,
|
| 187 |
+
"accession": alignment.accession,
|
| 188 |
+
"hit_id": alignment.hit_id,
|
| 189 |
+
"hit_def": alignment.hit_def,
|
| 190 |
+
"length": alignment.length,
|
| 191 |
+
"bit_score": float(hsp.bits),
|
| 192 |
+
"evalue": float(hsp.expect),
|
| 193 |
+
"identity_pct": round(identity_pct, 2),
|
| 194 |
+
"identities": identities,
|
| 195 |
+
"align_length": align_len
|
| 196 |
+
})
|
| 197 |
+
|
| 198 |
+
result = {
|
| 199 |
+
"query_length": len(sequence),
|
| 200 |
+
"database": database,
|
| 201 |
+
"program": program,
|
| 202 |
+
"total_hits": len(hits),
|
| 203 |
+
"hits": hits
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
with blast_lock:
|
| 207 |
+
blast_jobs[job_id]["status"] = "completed"
|
| 208 |
+
blast_jobs[job_id]["result"] = result
|
| 209 |
+
blast_jobs[job_id]["message"] = f"Completed: {len(hits)} hits found"
|
| 210 |
+
blast_jobs[job_id]["completed_at"] = time.time()
|
| 211 |
+
|
| 212 |
+
print(f"✅ [Job {job_id}] BLAST completed: {len(hits)} hits")
|
| 213 |
+
|
| 214 |
+
except Exception as e:
|
| 215 |
+
with blast_lock:
|
| 216 |
+
blast_jobs[job_id]["status"] = "failed"
|
| 217 |
+
blast_jobs[job_id]["error"] = str(e)
|
| 218 |
+
blast_jobs[job_id]["message"] = f"Failed: {str(e)}"
|
| 219 |
+
|
| 220 |
+
print(f"❌ [Job {job_id}] BLAST failed: {e}")
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@mcp.tool(name="blast_search_async", description="Start BLAST search asynchronously (returns job_id immediately)")
|
| 224 |
+
def blast_search_async(payload: dict):
|
| 225 |
+
"""
|
| 226 |
+
异步启动 BLAST 搜索,立即返回 job_id
|
| 227 |
+
Required fields: sequence
|
| 228 |
+
Optional fields: database (refseq_rna/nt), program (blastn), hitlist_size (20), expect (1e-10)
|
| 229 |
+
"""
|
| 230 |
+
try:
|
| 231 |
+
sequence = str(payload.get("sequence", "")).strip()
|
| 232 |
+
if not sequence:
|
| 233 |
+
return {"success": False, "result": None, "error": "sequence is required"}
|
| 234 |
+
|
| 235 |
+
sequence = "".join(sequence.split()).upper()
|
| 236 |
+
|
| 237 |
+
database = payload.get("database", "refseq_rna")
|
| 238 |
+
program = payload.get("program", "blastn")
|
| 239 |
+
hitlist_size = int(payload.get("hitlist_size", 20))
|
| 240 |
+
expect = float(payload.get("expect", 1e-10))
|
| 241 |
+
|
| 242 |
+
# 生成唯一 job_id
|
| 243 |
+
job_id = f"blast_{int(time.time() * 1000)}"
|
| 244 |
+
|
| 245 |
+
# 初始化 job 状态
|
| 246 |
+
with blast_lock:
|
| 247 |
+
blast_jobs[job_id] = {
|
| 248 |
+
"status": "pending",
|
| 249 |
+
"created_at": time.time(),
|
| 250 |
+
"sequence_length": len(sequence),
|
| 251 |
+
"database": database,
|
| 252 |
+
"message": "Job created, starting soon..."
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
# 在后台线程中运行 BLAST
|
| 256 |
+
thread = threading.Thread(
|
| 257 |
+
target=_run_blast_async,
|
| 258 |
+
args=(job_id, sequence, database, program, hitlist_size, expect),
|
| 259 |
+
daemon=True
|
| 260 |
+
)
|
| 261 |
+
thread.start()
|
| 262 |
+
|
| 263 |
+
print(f"🚀 BLAST job {job_id} started asynchronously")
|
| 264 |
+
|
| 265 |
+
return {
|
| 266 |
+
"success": True,
|
| 267 |
+
"result": {
|
| 268 |
+
"job_id": job_id,
|
| 269 |
+
"status": "pending",
|
| 270 |
+
"message": "BLAST search started. Use blast_check_status to check progress.",
|
| 271 |
+
"estimated_time": "1-15 minutes depending on database"
|
| 272 |
+
},
|
| 273 |
+
"error": None
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
except Exception as e:
|
| 277 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
@mcp.tool(name="blast_check_status", description="Check status of async BLAST job")
|
| 281 |
+
def blast_check_status(payload: dict):
|
| 282 |
+
"""
|
| 283 |
+
检查异步 BLAST 任务的状态
|
| 284 |
+
Required fields: job_id
|
| 285 |
+
"""
|
| 286 |
+
try:
|
| 287 |
+
job_id = payload.get("job_id", "")
|
| 288 |
+
if not job_id:
|
| 289 |
+
return {"success": False, "result": None, "error": "job_id is required"}
|
| 290 |
+
|
| 291 |
+
with blast_lock:
|
| 292 |
+
if job_id not in blast_jobs:
|
| 293 |
+
return {
|
| 294 |
+
"success": False,
|
| 295 |
+
"result": None,
|
| 296 |
+
"error": f"Job {job_id} not found"
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
job = blast_jobs[job_id].copy()
|
| 300 |
+
|
| 301 |
+
# 计算运行时间
|
| 302 |
+
elapsed = time.time() - job["created_at"]
|
| 303 |
+
job["elapsed_seconds"] = round(elapsed, 1)
|
| 304 |
+
|
| 305 |
+
return {
|
| 306 |
+
"success": True,
|
| 307 |
+
"result": job,
|
| 308 |
+
"error": None
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
except Exception as e:
|
| 312 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
@mcp.tool(name="blast_get_result", description="Get result of completed BLAST job")
|
| 316 |
+
def blast_get_result(payload: dict):
|
| 317 |
+
"""
|
| 318 |
+
获取已完成的 BLAST 任务结果
|
| 319 |
+
Required fields: job_id
|
| 320 |
+
"""
|
| 321 |
+
try:
|
| 322 |
+
job_id = payload.get("job_id", "")
|
| 323 |
+
if not job_id:
|
| 324 |
+
return {"success": False, "result": None, "error": "job_id is required"}
|
| 325 |
+
|
| 326 |
+
with blast_lock:
|
| 327 |
+
if job_id not in blast_jobs:
|
| 328 |
+
return {
|
| 329 |
+
"success": False,
|
| 330 |
+
"result": None,
|
| 331 |
+
"error": f"Job {job_id} not found"
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
job = blast_jobs[job_id]
|
| 335 |
+
|
| 336 |
+
if job["status"] != "completed":
|
| 337 |
+
return {
|
| 338 |
+
"success": False,
|
| 339 |
+
"result": None,
|
| 340 |
+
"error": f"Job {job_id} is not completed yet. Status: {job['status']}"
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
result = job.get("result")
|
| 344 |
+
|
| 345 |
+
return {
|
| 346 |
+
"success": True,
|
| 347 |
+
"result": result,
|
| 348 |
+
"error": None
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
except Exception as e:
|
| 352 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
# ==============================================
|
| 356 |
+
# BLAST 搜索工具 - BLAST Search Tools (同步版本,保留用于小序列)
|
| 357 |
# ==============================================
|
| 358 |
|
| 359 |
@mcp.tool(name="blast_search", description="Search NCBI database using BLAST to find similar sequences")
|
|
|
|
| 679 |
# 完整流程工具 - Complete Workflow Tool
|
| 680 |
# ==============================================
|
| 681 |
|
| 682 |
+
@mcp.tool(name="identify_gene_species_async",
|
| 683 |
+
description="Complete workflow with async BLAST - returns job_id immediately, check status separately")
|
| 684 |
+
def identify_gene_species_async(payload: dict):
|
| 685 |
+
"""
|
| 686 |
+
异步版本的完整基因物种鉴定流程
|
| 687 |
+
Required fields: sequence
|
| 688 |
+
Optional fields: min_identity (70), max_hits (20), database (refseq_rna)
|
| 689 |
+
|
| 690 |
+
Returns job_id immediately. Use these tools to track progress:
|
| 691 |
+
1. blast_check_status(job_id) - Check if BLAST is done
|
| 692 |
+
2. continue_workflow_after_blast(job_id) - Complete remaining steps after BLAST finishes
|
| 693 |
+
"""
|
| 694 |
+
try:
|
| 695 |
+
sequence = payload.get("sequence", "")
|
| 696 |
+
min_identity = float(payload.get("min_identity", 70.0))
|
| 697 |
+
max_hits = int(payload.get("max_hits", 20))
|
| 698 |
+
database = payload.get("database", "refseq_rna")
|
| 699 |
+
|
| 700 |
+
# Step 1: Validate sequence
|
| 701 |
+
print("📋 Step 1: Validating sequence...")
|
| 702 |
+
val_result = validate_sequence({"sequence": sequence})
|
| 703 |
+
|
| 704 |
+
if not val_result["success"]:
|
| 705 |
+
return {
|
| 706 |
+
"success": False,
|
| 707 |
+
"result": None,
|
| 708 |
+
"error": "Sequence validation failed"
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
validated_seq = val_result["result"]["sequence"]
|
| 712 |
+
|
| 713 |
+
# Step 2: Start async BLAST
|
| 714 |
+
print("🚀 Step 2: Starting async BLAST search...")
|
| 715 |
+
blast_async_result = blast_search_async({
|
| 716 |
+
"sequence": validated_seq,
|
| 717 |
+
"database": database,
|
| 718 |
+
"hitlist_size": max_hits
|
| 719 |
+
})
|
| 720 |
+
|
| 721 |
+
if not blast_async_result["success"]:
|
| 722 |
+
return blast_async_result
|
| 723 |
+
|
| 724 |
+
job_id = blast_async_result["result"]["job_id"]
|
| 725 |
+
|
| 726 |
+
# Store workflow parameters for later continuation
|
| 727 |
+
with blast_lock:
|
| 728 |
+
if job_id in blast_jobs:
|
| 729 |
+
blast_jobs[job_id]["workflow_params"] = {
|
| 730 |
+
"min_identity": min_identity,
|
| 731 |
+
"max_hits": max_hits,
|
| 732 |
+
"sequence_length": len(validated_seq)
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
return {
|
| 736 |
+
"success": True,
|
| 737 |
+
"result": {
|
| 738 |
+
"job_id": job_id,
|
| 739 |
+
"status": "blast_running",
|
| 740 |
+
"message": "BLAST search started. Use 'blast_check_status' to monitor progress, then 'continue_workflow_after_blast' to complete analysis.",
|
| 741 |
+
"next_steps": [
|
| 742 |
+
f"1. Check status: blast_check_status({{\"job_id\": \"{job_id}\"}})",
|
| 743 |
+
f"2. When completed: continue_workflow_after_blast({{\"job_id\": \"{job_id}\"}})"
|
| 744 |
+
]
|
| 745 |
+
},
|
| 746 |
+
"error": None
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
except Exception as e:
|
| 750 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 751 |
+
|
| 752 |
+
|
| 753 |
+
@mcp.tool(name="continue_workflow_after_blast",
|
| 754 |
+
description="Continue species identification workflow after BLAST completes")
|
| 755 |
+
def continue_workflow_after_blast(payload: dict):
|
| 756 |
+
"""
|
| 757 |
+
在 BLAST 完成后继续工作流程
|
| 758 |
+
Required fields: job_id
|
| 759 |
+
"""
|
| 760 |
+
try:
|
| 761 |
+
job_id = payload.get("job_id", "")
|
| 762 |
+
if not job_id:
|
| 763 |
+
return {"success": False, "result": None, "error": "job_id is required"}
|
| 764 |
+
|
| 765 |
+
# Get BLAST result
|
| 766 |
+
blast_result_response = blast_get_result({"job_id": job_id})
|
| 767 |
+
if not blast_result_response["success"]:
|
| 768 |
+
return blast_result_response
|
| 769 |
+
|
| 770 |
+
blast_result = blast_result_response["result"]
|
| 771 |
+
hits = blast_result["hits"]
|
| 772 |
+
|
| 773 |
+
# Get workflow parameters
|
| 774 |
+
with blast_lock:
|
| 775 |
+
workflow_params = blast_jobs[job_id].get("workflow_params", {})
|
| 776 |
+
|
| 777 |
+
min_identity = workflow_params.get("min_identity", 70.0)
|
| 778 |
+
|
| 779 |
+
workflow_results = {"steps": []}
|
| 780 |
+
|
| 781 |
+
# Step 3: Filter hits
|
| 782 |
+
print("🔬 Step 3: Filtering BLAST hits...")
|
| 783 |
+
filter_result = filter_blast_hits({
|
| 784 |
+
"hits": hits,
|
| 785 |
+
"min_identity": min_identity,
|
| 786 |
+
"max_hits": 10
|
| 787 |
+
})
|
| 788 |
+
workflow_results["steps"].append({
|
| 789 |
+
"step": 3,
|
| 790 |
+
"name": "filter_blast_hits",
|
| 791 |
+
"result": filter_result
|
| 792 |
+
})
|
| 793 |
+
|
| 794 |
+
filtered_hits = filter_result["result"]["hits"]
|
| 795 |
+
|
| 796 |
+
# Step 4: Extract species
|
| 797 |
+
print("🌍 Step 4: Extracting species information...")
|
| 798 |
+
species_result = extract_species_from_blast({"hits": filtered_hits})
|
| 799 |
+
workflow_results["steps"].append({
|
| 800 |
+
"step": 4,
|
| 801 |
+
"name": "extract_species_from_blast",
|
| 802 |
+
"result": species_result
|
| 803 |
+
})
|
| 804 |
+
|
| 805 |
+
species_data = species_result["result"]["species_data"]
|
| 806 |
+
|
| 807 |
+
# Step 5: Aggregate scores
|
| 808 |
+
print("📊 Step 5: Aggregating species scores...")
|
| 809 |
+
agg_result = aggregate_species_scores({"species_data": species_data})
|
| 810 |
+
workflow_results["steps"].append({
|
| 811 |
+
"step": 5,
|
| 812 |
+
"name": "aggregate_species_scores",
|
| 813 |
+
"result": agg_result
|
| 814 |
+
})
|
| 815 |
+
|
| 816 |
+
species_rankings = agg_result["result"]["species_rankings"]
|
| 817 |
+
|
| 818 |
+
# Step 6: Predict species
|
| 819 |
+
print("🎯 Step 6: Predicting species...")
|
| 820 |
+
pred_result = predict_species({"species_rankings": species_rankings})
|
| 821 |
+
workflow_results["steps"].append({
|
| 822 |
+
"step": 6,
|
| 823 |
+
"name": "predict_species",
|
| 824 |
+
"result": pred_result
|
| 825 |
+
})
|
| 826 |
+
|
| 827 |
+
# Compile final result
|
| 828 |
+
final_result = {
|
| 829 |
+
"job_id": job_id,
|
| 830 |
+
"input_sequence_length": workflow_params.get("sequence_length"),
|
| 831 |
+
"total_blast_hits": len(hits),
|
| 832 |
+
"filtered_hits": len(filtered_hits),
|
| 833 |
+
"unique_species_found": len(species_rankings),
|
| 834 |
+
"prediction": pred_result["result"] if pred_result["success"] else None,
|
| 835 |
+
"workflow": workflow_results
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
print("✅ Complete workflow finished successfully!")
|
| 839 |
+
|
| 840 |
+
return {
|
| 841 |
+
"success": True,
|
| 842 |
+
"result": final_result,
|
| 843 |
+
"error": None
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
except Exception as e:
|
| 847 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 848 |
+
|
| 849 |
+
|
| 850 |
@mcp.tool(name="identify_gene_species_complete",
|
| 851 |
description="Complete workflow: input unknown gene sequence, identify species through BLAST search and analysis")
|
| 852 |
def identify_gene_species_complete(payload: dict):
|