2026-08-03 18:42:47 +08:00
|
|
|
|
"""
|
|
|
|
|
|
Intent Parser - 硬编码规则解析用户意图
|
|
|
|
|
|
从 orchestrator.py 提取并扩展
|
|
|
|
|
|
"""
|
|
|
|
|
|
import re
|
|
|
|
|
|
from typing import Dict, List, Optional
|
|
|
|
|
|
|
|
|
|
|
|
from gfa_core.mas.stage_catalog import complete_dependencies as catalog_complete_dependencies
|
|
|
|
|
|
from gfa_core.mas.stage_catalog import STAGE_ORDER
|
|
|
|
|
|
|
|
|
|
|
|
# 常见基因家族
|
|
|
|
|
|
COMMON_FAMILIES = ['SPL', 'NAC', 'WRKY', 'MYB', 'bHLH', 'WD40', 'AP2', 'ERF']
|
|
|
|
|
|
|
|
|
|
|
|
# 需要排除的中文请求词(Python 3 的 \w 匹配 Unicode 字母)
|
|
|
|
|
|
EXCLUDE_WORDS = {
|
|
|
|
|
|
'帮我', '执行', '分析', '进行', '运行', '开始', '帮我执行',
|
|
|
|
|
|
'帮我运行', '请帮我', '请你', '想要', '希望', '需要',
|
|
|
|
|
|
'鉴定分析', '进化分析', '表达分析', '结构分析', '启动子分析',
|
|
|
|
|
|
'共线性分析', '核型分析', '蛋白分析', 'motif分析', 'domain分析',
|
|
|
|
|
|
'ID', 'IDS'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 核心分析阶段(完整流程用)- 必须与 manifest 中的 stage 字段一致。
|
|
|
|
|
|
# “基因家族分析/全流程”默认不包含 Expression;Expression 需要外部
|
|
|
|
|
|
# FPKM/TPM 矩阵和 sample.txt,只有用户明确点名表达分析时才加入。
|
|
|
|
|
|
# KaKs/PPI 需要 Ortholog,作为可执行上游阶段显式纳入全流程计划。
|
|
|
|
|
|
CORE_STAGES = [
|
|
|
|
|
|
"identify", "phylogeny", "motif", "domain", "structure", "combo",
|
|
|
|
|
|
"promoter", "tfbs", "synteny", "interspecies_synteny", "karyotype", "pep",
|
|
|
|
|
|
"ortholog", "kaks", "ppi"
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# 完整流程关键词
|
|
|
|
|
|
COMPLETE_WORKFLOW_KEYWORDS = [
|
|
|
|
|
|
"基因家族分析", "完整分析", "全流程", "全部阶段", "完整流程"
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# 分析阶段关键词映射(key 必须与 manifest 中的 stage 字段一致)
|
|
|
|
|
|
STAGE_KEYWORDS = {
|
|
|
|
|
|
"gwas_fmt": ["gwas格式", "gwas format", "tassel转换", "tassel convert", "hapmap转换", "vcf转hapmap", "vcf to hapmap"],
|
|
|
|
|
|
"identify": ["identify", "鉴定", "鉴定分析", "基因鉴定", "gene-identify"],
|
|
|
|
|
|
"identify_refine": ["修订成员", "修订鉴定成员", "修改成员", "删除基因", "删除", "去除基因", "去掉基因", "移除基因", "补充基因", "遗漏基因", "重建蛋白", "人工修订id", "curated members", "refine identify"],
|
|
|
|
|
|
"phylogeny": ["phylogeny", "进化", "进化树", "系统发育", "树"],
|
|
|
|
|
|
"reference_classification": ["预测亚家族", "参考亚家族", "亚家族分类", "参考分类", "亚家族", "subfamily prediction", "subfamily", "reference classification", "reference_classification"],
|
|
|
|
|
|
"subfamily_confirm": ["确认亚家族", "亚家族确认", "确认亚家族标签", "确认分类", "人工确认亚家族", "confirmed subfamily", "subfamily confirm", "subfamily_confirm"],
|
|
|
|
|
|
"motif": ["motif", "基序", "motif分析", "motif-analysis"],
|
|
|
|
|
|
"domain": ["domain", "结构域", "domain分析", "pfam", "domain-analysis"],
|
|
|
|
|
|
"structure": ["structure", "基因结构", "结构分析", "蛋白结构"],
|
|
|
|
|
|
"promoter_plot_revise": ["promoter_plot_revise", "启动子diy", "启动子 diy", "启动子自定义", "启动子自定义绘图", "启动子重绘", "启动子二次绘图", "plantcare重绘", "promoter custom", "promoter replot", "diy绘图"],
|
|
|
|
|
|
"tfbs": [
|
|
|
|
|
|
"tfbs", "fimo", "tf结合", "tf 结合", "转录因子结合", "转录因子结合位点",
|
|
|
|
|
|
"启动子结合位点", "结合位点", "binding site", "binding-site", "binding_site",
|
|
|
|
|
|
"tf motif", "tf motif scanning", "motif binding"
|
|
|
|
|
|
],
|
|
|
|
|
|
"promoter": [
|
|
|
|
|
|
"promoter", "启动子", "启动子分析", "顺式", "顺式元件", "plantcare",
|
|
|
|
|
|
"cis-element", "cis element", "promoter-analysis"
|
|
|
|
|
|
],
|
|
|
|
|
|
"synteny": ["synteny", "种内共线性", "种内共线性分析", "mcscanx", "intraspecies"],
|
|
|
|
|
|
"karyotype": ["karyotype", "核型", "染色体分布", "核型分析", "染色体定位"],
|
|
|
|
|
|
"interspecies_synteny": ["interspecies", "种间", "种间共线性", "跨物种"],
|
|
|
|
|
|
"expression": ["expression", "表达", "表达分析", "表达量"],
|
|
|
|
|
|
"kaks": ["kaks", "kaks分析", "进化速率", "选择压力", "ka/ks"],
|
|
|
|
|
|
"ppi": ["ppi", "蛋白互作", "互作网络", "protein interaction"],
|
|
|
|
|
|
"pep": ["pep", "蛋白特征", "蛋白性质", "等电点", "分子量", "亚细胞定位"],
|
|
|
|
|
|
"combo": ["combo", "组合图", "综合图", "组合可视化", "tree motif structure domain", "tree+motif"],
|
|
|
|
|
|
"annotation": ["背景注释", "功能注释", "基因组注释", "自动注释", "eggnog", "annotation"],
|
|
|
|
|
|
"genome_prepare": ["数据准备", "基因组准备", "genome_prepare", "prepare genome"],
|
|
|
|
|
|
"sequence_extract_by_id": ["按id提取序列", "按id提取蛋白", "按id提取cds", "从pep中提取", "从fasta中提取", "extract sequence by id"],
|
|
|
|
|
|
"msa_region_plot": ["msa区间", "msa绘图", "msa二次绘图", "msa局部绘图", "多序列比对区间", "比对区间绘图", "保守区间绘图", "保守位置绘图", "alignment region", "msa region"],
|
|
|
|
|
|
"prepare_organize": ["整理文件", "组织文件", "整理数据", "标准化命名", "重命名文件", "config整理"],
|
|
|
|
|
|
"prepare_normalize_gff": ["修订gff", "规范化gff", "gff id", "修订id", "规范化id", "gff整理"],
|
|
|
|
|
|
"prepare_longest_transcript": ["最长转录本", "最长cds", "primary transcript", "longest transcript", "提取最长"],
|
|
|
|
|
|
"prepare_extract_pep": ["提取pep", "提取蛋白", "提取蛋白序列", "gffread pep", "extract pep"],
|
|
|
|
|
|
"prepare_extract_cds": ["提取cds", "提取cds序列", "gffread cds", "extract cds"],
|
|
|
|
|
|
"prepare_filter_fasta": ["按id提取", "按id筛选", "提取fasta", "筛选fasta", "subseq"],
|
|
|
|
|
|
"prepare_simplify_ncbi": ["ncbi id", "ncbi修订", "ncbi简化", "ncbi header", "简化ncbi"],
|
|
|
|
|
|
# RNA-seq 相关(独立流程)
|
|
|
|
|
|
"rnaseq_qc": ["rnaseq质控", "质控", "qc"],
|
|
|
|
|
|
"rnaseq_align": ["rnaseq比对", "比对", "align", "hisat2"],
|
|
|
|
|
|
"rnaseq_quant": ["rnaseq定量", "定量", "quant", "featurecounts"],
|
|
|
|
|
|
"rnaseq_sample_qc": ["样本质量", "样本检测", "sample qc", "sample quality", "pca", "样本相关性", "样本聚类"],
|
|
|
|
|
|
"rnaseq_wgcna": ["wgcna", "共表达", "加权共表达", "模块性状", "module trait"],
|
|
|
|
|
|
"rnaseq_de": ["差异表达", "de", "差异分析", "deseq2"],
|
|
|
|
|
|
"rnaseq_fgsea": ["fgsea", "gsea", "排序富集", "基因集富集"],
|
|
|
|
|
|
"rnaseq_go_enrich": ["rnaseq go富集", "rnaseq_go富集", "rna go富集", "go富集", "go分析", "富集分析", "enrich"],
|
|
|
|
|
|
"rnaseq_kegg_enrich": ["rnaseq kegg", "rnaseq_kegg", "rna kegg", "kegg富集", "kegg分析", "kegg", "富集分析", "enrich"],
|
|
|
|
|
|
"rnaseq_enrich": ["富集分析", "enrich", "go分析", "kegg", "clusterprofiler"],
|
|
|
|
|
|
"rnaseq_candidate_family": ["候选家族", "候选基因家族", "家族排名", "tf注释", "tf family", "tffamily", "pfam注释", "deg注释", "差异基因注释", "差异基因家族", "candidate family"],
|
|
|
|
|
|
"ncbi_fetch_sequences": ["ncbi下载", "ncbi序列", "下载序列", "下载蛋白", "获取蛋白", "下载protein", "下载cds", "protein和cds", "蛋白和cds", "基因list", "gene list", "gene_list", "id列表", ".ids"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_gwas_fmt_intent(query: str) -> bool:
|
|
|
|
|
|
"""Return whether a command explicitly requests GWAS format conversion."""
|
|
|
|
|
|
query_lower = str(query or "").lower()
|
|
|
|
|
|
compact = re.sub(r"[\s_-]+", "", query_lower)
|
|
|
|
|
|
explicit_conversion = any(
|
|
|
|
|
|
keyword in compact
|
|
|
|
|
|
for keyword in (
|
|
|
|
|
|
"gwas格式",
|
|
|
|
|
|
"gwasformat",
|
|
|
|
|
|
"tassel转换",
|
|
|
|
|
|
"tasselconvert",
|
|
|
|
|
|
"hapmap转换",
|
|
|
|
|
|
"vcf格式转换",
|
|
|
|
|
|
"vcf转hapmap",
|
|
|
|
|
|
"vcf转换为hapmap",
|
|
|
|
|
|
"vcftohapmap",
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return explicit_conversion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_generic_gwas_intent(query: str) -> bool:
|
|
|
|
|
|
"""Return whether the request selects the complete GWAS workflow."""
|
|
|
|
|
|
resolved = resolve_gwas_intent(query)
|
|
|
|
|
|
return bool(resolved and resolved.get("full_chain"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_population_input_params(query: str) -> Dict[str, str]:
|
|
|
|
|
|
"""Extract explicitly named population VCF and sample-group paths."""
|
|
|
|
|
|
text = re.sub(r"\s*/\s*", "/", str(query or "").strip())
|
|
|
|
|
|
params = {"prefix": "cohort"}
|
|
|
|
|
|
|
|
|
|
|
|
def anchored_path(anchor: str, suffix: str) -> Optional[str]:
|
|
|
|
|
|
quoted = re.search(
|
|
|
|
|
|
rf"(?:{anchor})\s*(?:文件)?\s*[=::]?\s*[\"“']([^\"”']+?{suffix})[\"”']",
|
|
|
|
|
|
text,
|
|
|
|
|
|
flags=re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
if quoted:
|
|
|
|
|
|
return quoted.group(1).strip()
|
|
|
|
|
|
unquoted = re.search(
|
|
|
|
|
|
rf"(?:{anchor})\s*(?:文件)?\s*[=::]?\s*([^\s,。;,;\"“”']+?{suffix})(?=$|[\s,。;,;])",
|
|
|
|
|
|
text,
|
|
|
|
|
|
flags=re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
return unquoted.group(1).strip() if unquoted else None
|
|
|
|
|
|
|
|
|
|
|
|
vcf = anchored_path(r"使用|use|vcf", r"\.vcf(?:\.gz)?")
|
|
|
|
|
|
if vcf:
|
|
|
|
|
|
params["populationVcf"] = vcf
|
|
|
|
|
|
group = anchored_path(r"分组表|group\s+table|sample\s+group", r"\.(?:tsv|txt|csv)")
|
|
|
|
|
|
if group:
|
|
|
|
|
|
params["populationSampleGroup"] = group
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_population_intent(query: str) -> Optional[Dict]:
|
|
|
|
|
|
"""Resolve population-genomics requests without stealing gene-family trees."""
|
|
|
|
|
|
text = str(query or "").lower()
|
|
|
|
|
|
compact = re.sub(r"[\s_-]+", "", text)
|
|
|
|
|
|
if any(token in compact for token in ("基因家族", "genefamily")):
|
|
|
|
|
|
return None
|
|
|
|
|
|
target: Optional[str] = None
|
|
|
|
|
|
full_chain = False
|
|
|
|
|
|
population_context = any(token in compact for token in ("群体", "population", "群体遗传"))
|
|
|
|
|
|
if not population_context:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if any(token in compact for token in ("群体系统树", "群体进化树", "populationphylogeny", "populationtree")):
|
|
|
|
|
|
target = "pop_phylogeny"
|
|
|
|
|
|
elif any(token in compact for token in ("群体结构", "admixture")):
|
|
|
|
|
|
target = "pop_structure"
|
|
|
|
|
|
elif any(token in compact for token in ("ld衰减", "连锁不平衡衰减", "lddecay")):
|
|
|
|
|
|
target = "pop_ld_decay"
|
|
|
|
|
|
elif any(token in compact for token in ("snp密度", "变异密度", "snpdensity")):
|
|
|
|
|
|
target = "pop_snp_density"
|
|
|
|
|
|
elif "pca" in compact or "主成分" in compact:
|
|
|
|
|
|
target = "pop_pca"
|
|
|
|
|
|
elif any(token in compact for token in ("群体遗传分析", "群体基因组分析", "populationgenomics")):
|
|
|
|
|
|
target = "pop_snp_density"
|
|
|
|
|
|
full_chain = True
|
|
|
|
|
|
if target is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
stages = ["pop_pca", "pop_phylogeny", "pop_structure", "pop_ld_decay", "pop_snp_density"] if full_chain else [target]
|
|
|
|
|
|
return {
|
|
|
|
|
|
"target": target,
|
|
|
|
|
|
"stages": stages,
|
|
|
|
|
|
"full_chain": full_chain,
|
|
|
|
|
|
"prefix": "cohort",
|
|
|
|
|
|
"params": parse_population_input_params(query),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_gwas_intent(query: str) -> Optional[Dict]:
|
|
|
|
|
|
"""Resolve one deterministic GWAS target and its dependency closure."""
|
|
|
|
|
|
text = str(query or "").lower()
|
|
|
|
|
|
compact = re.sub(r"[\s_-]+", "", text)
|
|
|
|
|
|
target: Optional[str] = None
|
|
|
|
|
|
full_chain = False
|
|
|
|
|
|
|
|
|
|
|
|
if is_gwas_fmt_intent(query):
|
|
|
|
|
|
target = "gwas_fmt"
|
|
|
|
|
|
elif "emmax" in compact and any(word in compact for word in ("绘图", "作图", "manhattan", "qq图", "plot")):
|
|
|
|
|
|
target = "gwas_emplot"
|
|
|
|
|
|
elif ("mlm" in compact or "cmlm" in compact) and any(
|
|
|
|
|
|
word in compact for word in ("绘图", "作图", "manhattan", "qq图", "plot")
|
|
|
|
|
|
):
|
|
|
|
|
|
target = "gwas_plot"
|
|
|
|
|
|
elif any(word in compact for word in ("亲缘关系", "亲缘矩阵", "kinship")):
|
|
|
|
|
|
target = "gwas_kinship"
|
|
|
|
|
|
elif any(word in compact for word in ("表型准备", "表型数据", "性状准备", "traitprepare")):
|
|
|
|
|
|
target = "gwas_trait"
|
|
|
|
|
|
elif any(word in compact for word in ("协变量", "covariate", "q矩阵", "qmatrix")):
|
|
|
|
|
|
target = "gwas_cov"
|
|
|
|
|
|
elif "emmax" in compact:
|
|
|
|
|
|
target = "gwas_emmax"
|
|
|
|
|
|
elif "cmlm" in compact or "mlm" in compact or "混合线性模型" in compact:
|
|
|
|
|
|
target = "gwas_mlm"
|
|
|
|
|
|
elif "glm" in compact or "一般线性模型" in compact or "固定效应模型" in compact:
|
|
|
|
|
|
target = "gwas_glm"
|
|
|
|
|
|
elif "gwas" in compact and any(word in compact for word in ("绘图", "作图", "manhattan", "qq图", "plot")):
|
|
|
|
|
|
target = "gwas_plot"
|
|
|
|
|
|
elif "gwas" in compact:
|
|
|
|
|
|
target = "gwas_glm"
|
|
|
|
|
|
full_chain = True
|
|
|
|
|
|
|
|
|
|
|
|
if target is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
targets = ["gwas_plot", "gwas_emplot", "gwas_glm"] if full_chain else [target]
|
|
|
|
|
|
return {
|
|
|
|
|
|
"target": target,
|
|
|
|
|
|
"stages": catalog_complete_dependencies(targets),
|
|
|
|
|
|
"full_chain": full_chain,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# These are direct utility/repair operations, not normal pipeline-analysis
|
|
|
|
|
|
# stages. They are handled with stricter context below so broad words such as
|
|
|
|
|
|
# "提取蛋白" or "修订ID" cannot steal a gene-family/RNA-seq pipeline request.
|
|
|
|
|
|
UTILITY_KEYWORD_STAGES = {
|
|
|
|
|
|
"sequence_extract_by_id",
|
|
|
|
|
|
"msa_region_plot",
|
|
|
|
|
|
"promoter_plot_revise",
|
|
|
|
|
|
"prepare_organize",
|
|
|
|
|
|
"prepare_normalize_gff",
|
|
|
|
|
|
"prepare_longest_transcript",
|
|
|
|
|
|
"prepare_extract_pep",
|
|
|
|
|
|
"prepare_extract_cds",
|
|
|
|
|
|
"prepare_filter_fasta",
|
|
|
|
|
|
"prepare_simplify_ncbi",
|
|
|
|
|
|
"ncbi_fetch_sequences",
|
|
|
|
|
|
}
|
|
|
|
|
|
REPAIR_KEYWORD_STAGES = {"identify_refine", "subfamily_confirm"}
|
|
|
|
|
|
|
|
|
|
|
|
RNASEQ_MARKERS = ["rnaseq", "rna-seq", "rna seq", "转录组", "fastq"]
|
|
|
|
|
|
RNASEQ_STAGES = {
|
|
|
|
|
|
"rnaseq_qc",
|
|
|
|
|
|
"rnaseq_align",
|
|
|
|
|
|
"rnaseq_quant",
|
|
|
|
|
|
"rnaseq_sample_qc",
|
|
|
|
|
|
"rnaseq_wgcna",
|
|
|
|
|
|
"rnaseq_de",
|
|
|
|
|
|
"rnaseq_fgsea",
|
|
|
|
|
|
"rnaseq_go_enrich",
|
|
|
|
|
|
"rnaseq_kegg_enrich",
|
|
|
|
|
|
"rnaseq_enrich",
|
|
|
|
|
|
"rnaseq_candidate_family",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 物种关键词映射
|
|
|
|
|
|
SPECIES_KEYWORDS = {
|
|
|
|
|
|
"Ft": ["ft", "tartary", "苦荞", "荞麦"],
|
|
|
|
|
|
"At": ["at", "arabidopsis", "拟南芥"],
|
|
|
|
|
|
"Os": ["os", "rice", "水稻", "稻"],
|
|
|
|
|
|
"Ls": ["ls", "lettuce", "lactuca", "生菜", "莴苣"],
|
|
|
|
|
|
"Zm": ["zm", "maize", "玉米"],
|
|
|
|
|
|
"Sl": ["sl", "tomato", "番茄"],
|
|
|
|
|
|
"Lc": ["lc", "litchi", "荔枝"],
|
|
|
|
|
|
"Dl": ["dl", "longan", "龙眼"],
|
|
|
|
|
|
"Ppy": ["ppy", "ppyrus", "pyrus"],
|
|
|
|
|
|
"Py": ["py"],
|
|
|
|
|
|
"Cp": ["cp", "papaya", "木瓜"],
|
|
|
|
|
|
"Vv": ["vv", "grape", "葡萄"],
|
|
|
|
|
|
"Lg": ["lg"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
SPECIES_TO_LATIN = {
|
|
|
|
|
|
"At": "Arabidopsis thaliana",
|
|
|
|
|
|
"Os": "Oryza sativa",
|
|
|
|
|
|
"Ls": "Lactuca sativa",
|
|
|
|
|
|
"Zm": "Zea mays",
|
|
|
|
|
|
"Sl": "Solanum lycopersicum",
|
|
|
|
|
|
"Ft": "Fagopyrum tataricum",
|
|
|
|
|
|
"Lc": "Litchi chinensis",
|
|
|
|
|
|
"Dl": "Dimocarpus longan",
|
|
|
|
|
|
"Ppy": "Pyrus pyrifolia",
|
|
|
|
|
|
"Py": "",
|
|
|
|
|
|
"Cp": "Carica papaya",
|
|
|
|
|
|
"Vv": "Vitis vinifera",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
SPECIES_TO_TAXID = {
|
|
|
|
|
|
"At": "3702",
|
|
|
|
|
|
"Os": "4530",
|
|
|
|
|
|
"Zm": "4577",
|
|
|
|
|
|
"Sl": "4081",
|
|
|
|
|
|
"Ft": "62330",
|
|
|
|
|
|
"Vv": "29760",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _unique_keep_order(items: List[str]) -> List[str]:
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
ordered = []
|
|
|
|
|
|
for item in items:
|
|
|
|
|
|
if item and item not in seen:
|
|
|
|
|
|
seen.add(item)
|
|
|
|
|
|
ordered.append(item)
|
|
|
|
|
|
return ordered
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _species_mentions_with_roles(query: str) -> Dict[str, List[str]]:
|
|
|
|
|
|
text = str(query or "")
|
|
|
|
|
|
lower = text.lower()
|
|
|
|
|
|
|
|
|
|
|
|
def collect_species(segment: str) -> List[str]:
|
|
|
|
|
|
segment_lower = segment.lower()
|
|
|
|
|
|
found: List[str] = []
|
|
|
|
|
|
for match in re.finditer(r"(?<![A-Za-z0-9])(?:Ft|At|Os|Ls|Zm|Sl|Lc|Dl|Ppy|Py|Cp|Vv|Lg)(?![A-Za-z0-9])", segment, flags=re.IGNORECASE):
|
|
|
|
|
|
code = canonical.get(match.group(0).lower())
|
|
|
|
|
|
if code:
|
|
|
|
|
|
found.append(code)
|
|
|
|
|
|
for code, keywords in SPECIES_KEYWORDS.items():
|
|
|
|
|
|
for keyword in keywords:
|
|
|
|
|
|
if len(keyword) <= 2 and keyword.lower() == code.lower():
|
|
|
|
|
|
continue
|
|
|
|
|
|
if re.search(re.escape(keyword.lower()), segment_lower):
|
|
|
|
|
|
found.append(code)
|
|
|
|
|
|
return _unique_keep_order(found)
|
|
|
|
|
|
|
|
|
|
|
|
canonical = {code.lower(): code for code in SPECIES_KEYWORDS}
|
|
|
|
|
|
role_markers: List[tuple[int, str]] = []
|
|
|
|
|
|
marker_patterns = [
|
|
|
|
|
|
("targetSpecies", r"目标物种|目标|target|待鉴定|研究物种"),
|
|
|
|
|
|
(
|
|
|
|
|
|
"referenceSpecies",
|
|
|
|
|
|
r"参考物种|参考|(?<![A-Za-z0-9_])reference(?![A-Za-z0-9_])|"
|
|
|
|
|
|
r"(?<![A-Za-z0-9_])ref(?![A-Za-z0-9_])|seed_reference|已知家族",
|
|
|
|
|
|
),
|
|
|
|
|
|
("comparativeSpecies", r"比较物种|比较|对比|comparative|comparison|outgroup|外群"),
|
|
|
|
|
|
]
|
|
|
|
|
|
roles = {"targetSpecies": [], "referenceSpecies": [], "comparativeSpecies": [], "unassigned": []}
|
|
|
|
|
|
# A role may follow the species, as in "Ft是目标物种". Resolve
|
|
|
|
|
|
# comma-delimited clauses first so a path fragment such as ref_data cannot
|
|
|
|
|
|
# assign a role or reverse the explicit Ft/At relationship.
|
|
|
|
|
|
for clause in re.split(r"[,,;;。\n]+", text):
|
|
|
|
|
|
clause_lower = clause.lower()
|
|
|
|
|
|
for role, pattern in marker_patterns:
|
|
|
|
|
|
if re.search(pattern, clause_lower):
|
|
|
|
|
|
roles[role].extend(collect_species(clause))
|
|
|
|
|
|
if any(roles[role] for role in ("targetSpecies", "referenceSpecies", "comparativeSpecies")):
|
|
|
|
|
|
assigned = set(roles["targetSpecies"] + roles["referenceSpecies"] + roles["comparativeSpecies"])
|
|
|
|
|
|
roles["unassigned"] = [sp for sp in collect_species(text) if sp not in assigned]
|
|
|
|
|
|
return {key: _unique_keep_order(value) for key, value in roles.items()}
|
|
|
|
|
|
|
|
|
|
|
|
for role, pattern in marker_patterns:
|
|
|
|
|
|
for match in re.finditer(pattern, lower):
|
|
|
|
|
|
role_markers.append((match.start(), role))
|
|
|
|
|
|
role_markers.sort(key=lambda x: x[0])
|
|
|
|
|
|
if role_markers:
|
|
|
|
|
|
for idx, (start, role) in enumerate(role_markers):
|
|
|
|
|
|
end = role_markers[idx + 1][0] if idx + 1 < len(role_markers) else len(text)
|
|
|
|
|
|
roles[role].extend(collect_species(text[start:end]))
|
|
|
|
|
|
assigned = set(roles["targetSpecies"] + roles["referenceSpecies"] + roles["comparativeSpecies"])
|
|
|
|
|
|
all_species = collect_species(text)
|
|
|
|
|
|
roles["unassigned"] = [sp for sp in all_species if sp not in assigned]
|
|
|
|
|
|
return {key: _unique_keep_order(value) for key, value in roles.items()}
|
|
|
|
|
|
|
|
|
|
|
|
mentions: List[tuple[int, str]] = []
|
|
|
|
|
|
code_pattern = r"(?<![A-Za-z0-9])(?:Ft|At|Os|Ls|Zm|Sl|Lc|Dl|Ppy|Py|Cp|Vv|Lg)(?![A-Za-z0-9])"
|
|
|
|
|
|
for match in re.finditer(code_pattern, text, flags=re.IGNORECASE):
|
|
|
|
|
|
code = canonical.get(match.group(0).lower())
|
|
|
|
|
|
if code:
|
|
|
|
|
|
mentions.append((match.start(), code))
|
|
|
|
|
|
for code, keywords in SPECIES_KEYWORDS.items():
|
|
|
|
|
|
for keyword in keywords:
|
|
|
|
|
|
if len(keyword) <= 2 and keyword.lower() == code.lower():
|
|
|
|
|
|
continue
|
|
|
|
|
|
for match in re.finditer(re.escape(keyword.lower()), lower):
|
|
|
|
|
|
mentions.append((match.start(), code))
|
|
|
|
|
|
|
|
|
|
|
|
for pos, code in sorted(mentions, key=lambda x: x[0]):
|
|
|
|
|
|
window = lower[max(0, pos - 18): pos]
|
|
|
|
|
|
if any(token in window for token in ["参考", "reference", "ref", "seed_reference", "已知家族"]):
|
|
|
|
|
|
roles["referenceSpecies"].append(code)
|
|
|
|
|
|
elif any(token in window for token in ["比较", "对比", "comparative", "comparison", "outgroup", "外群"]):
|
|
|
|
|
|
roles["comparativeSpecies"].append(code)
|
|
|
|
|
|
elif any(token in window for token in ["目标", "target", "待鉴定", "研究物种"]):
|
|
|
|
|
|
roles["targetSpecies"].append(code)
|
|
|
|
|
|
else:
|
|
|
|
|
|
roles["unassigned"].append(code)
|
|
|
|
|
|
return {key: _unique_keep_order(value) for key, value in roles.items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_gene_family_scope_selection(query: str) -> Dict[str, object]:
|
|
|
|
|
|
"""Parse target/reference/comparative species selection from free text."""
|
|
|
|
|
|
roles = _species_mentions_with_roles(query)
|
|
|
|
|
|
target = roles["targetSpecies"] or roles["unassigned"]
|
|
|
|
|
|
reference = [sp for sp in roles["referenceSpecies"] if sp not in target]
|
|
|
|
|
|
comparative = [sp for sp in roles["comparativeSpecies"] if sp not in target and sp not in reference]
|
|
|
|
|
|
if reference and comparative:
|
|
|
|
|
|
scope_mode = "target_plus_reference_plus_comparative"
|
|
|
|
|
|
elif reference:
|
|
|
|
|
|
scope_mode = "target_plus_reference"
|
|
|
|
|
|
elif comparative:
|
|
|
|
|
|
scope_mode = "target_plus_comparative"
|
|
|
|
|
|
else:
|
|
|
|
|
|
scope_mode = "target_only"
|
|
|
|
|
|
return {
|
|
|
|
|
|
"targetSpecies": target,
|
|
|
|
|
|
"referenceSpecies": reference,
|
|
|
|
|
|
"comparativeSpecies": comparative,
|
|
|
|
|
|
"scopeMode": scope_mode,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-05 15:42:32 +08:00
|
|
|
|
def parse_explicit_species_role_mentions(query: str) -> Dict[str, List[str]]:
|
|
|
|
|
|
"""Return explicit roles without promoting unassigned species to targets."""
|
|
|
|
|
|
text = str(query or "")
|
|
|
|
|
|
|
|
|
|
|
|
def mentioned_species(value: str) -> List[str]:
|
|
|
|
|
|
parsed = _species_mentions_with_roles(value)
|
|
|
|
|
|
return _unique_keep_order([
|
|
|
|
|
|
species
|
|
|
|
|
|
for key in ("targetSpecies", "referenceSpecies", "comparativeSpecies", "unassigned")
|
|
|
|
|
|
for species in parsed.get(key) or []
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
explicit_patterns = {
|
|
|
|
|
|
"targetSpecies": r"目标物种|目标基因组|作为目标|是目标|\btarget(?:\s+species|\s+genome)?\b",
|
|
|
|
|
|
"referenceSpecies": r"参考物种|参考基因组|作为参考|是参考|\breference(?:\s+species|\s+genome)?\b|\bseed_reference\b|\bknown_family_reference\b",
|
|
|
|
|
|
"comparativeSpecies": r"比较物种|比较基因组|作为比较|是比较|作为外群|是外群|\bcomparative(?:\s+species|\s+genome)?\b|\boutgroup\b",
|
|
|
|
|
|
}
|
|
|
|
|
|
roles: Dict[str, List[str]] = {
|
|
|
|
|
|
"targetSpecies": [],
|
|
|
|
|
|
"referenceSpecies": [],
|
|
|
|
|
|
"comparativeSpecies": [],
|
|
|
|
|
|
"unassigned": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
for clause in re.split(r"[,,;;。\n]+", text):
|
|
|
|
|
|
clause_species = mentioned_species(clause)
|
|
|
|
|
|
matched_roles = [
|
|
|
|
|
|
key
|
|
|
|
|
|
for key, pattern in explicit_patterns.items()
|
|
|
|
|
|
if re.search(pattern, clause, flags=re.IGNORECASE)
|
|
|
|
|
|
]
|
|
|
|
|
|
if len(matched_roles) == 1:
|
|
|
|
|
|
roles[matched_roles[0]].extend(clause_species)
|
|
|
|
|
|
|
|
|
|
|
|
assigned = set(
|
|
|
|
|
|
roles["targetSpecies"]
|
|
|
|
|
|
+ roles["referenceSpecies"]
|
|
|
|
|
|
+ roles["comparativeSpecies"]
|
|
|
|
|
|
)
|
|
|
|
|
|
roles["unassigned"] = [
|
|
|
|
|
|
species for species in mentioned_species(text) if species not in assigned
|
|
|
|
|
|
]
|
|
|
|
|
|
return {key: _unique_keep_order(value) for key, value in roles.items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 18:42:47 +08:00
|
|
|
|
def parse_promoter_plot_revise_request(query: str) -> Dict:
|
|
|
|
|
|
"""Extract parameters for custom promoter cis-element plotting."""
|
|
|
|
|
|
text = str(query or "").strip()
|
|
|
|
|
|
lower = text.lower()
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
has_promoter = any(kw in lower for kw in ["promoter", "plantcare", "启动子", "顺式"])
|
|
|
|
|
|
has_custom_plot = any(kw in lower for kw in [
|
|
|
|
|
|
"promoter_plot_revise", "diy", "自定义", "重绘", "二次绘图", "只看", "只绘制", "只画",
|
|
|
|
|
|
"不重新跑", "不重新提交", "已有启动子", "已有 plantcare", "已有plantcare", "custom", "replot",
|
|
|
|
|
|
])
|
|
|
|
|
|
has_known_param = any(kw in text for kw in ["promoterElements", "promoterCategories", "promoterMatchMode", "promoterPlotModes", "promoterColorBy"])
|
|
|
|
|
|
if not ((has_promoter and has_custom_plot) or has_known_param):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
params: Dict[str, object] = {}
|
|
|
|
|
|
prefix = _extract_prefix(text)
|
|
|
|
|
|
if prefix:
|
|
|
|
|
|
params["prefix"] = prefix
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_named(name: str) -> str:
|
|
|
|
|
|
match = re.search(
|
|
|
|
|
|
rf"{re.escape(name)}\s*[=::]\s*(.*?)(?=\s*[,,;;。]?\s*promoter(?:Elements|Categories|MatchMode|PlotModes|TopN|OutputLabel)\s*[=::]|[。;;\n]|$)",
|
|
|
|
|
|
text,
|
|
|
|
|
|
flags=re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
return match.group(1).strip().strip(",,。;; ") if match else ""
|
|
|
|
|
|
|
|
|
|
|
|
named_elements = _extract_named("promoterElements")
|
|
|
|
|
|
named_categories = _extract_named("promoterCategories")
|
|
|
|
|
|
named_match = _extract_named("promoterMatchMode")
|
|
|
|
|
|
named_modes = _extract_named("promoterPlotModes")
|
|
|
|
|
|
named_color = _extract_named("promoterColorBy")
|
|
|
|
|
|
named_label = _extract_named("promoterOutputLabel")
|
|
|
|
|
|
|
|
|
|
|
|
known_categories = [
|
|
|
|
|
|
"Core promoter", "Light response", "Hormone signaling", "Stress response", "TF binding", "Development", "Other",
|
|
|
|
|
|
]
|
|
|
|
|
|
category_aliases = {
|
|
|
|
|
|
"core promoter": "Core promoter",
|
|
|
|
|
|
"light response": "Light response",
|
|
|
|
|
|
"hormone signaling": "Hormone signaling",
|
|
|
|
|
|
"stress response": "Stress response",
|
|
|
|
|
|
"tf binding": "TF binding",
|
|
|
|
|
|
"development": "Development",
|
|
|
|
|
|
"other": "Other",
|
|
|
|
|
|
"核心启动子": "Core promoter",
|
|
|
|
|
|
"光响应": "Light response",
|
|
|
|
|
|
"激素": "Hormone signaling",
|
|
|
|
|
|
"激素响应": "Hormone signaling",
|
|
|
|
|
|
"胁迫": "Stress response",
|
|
|
|
|
|
"胁迫响应": "Stress response",
|
|
|
|
|
|
"转录因子": "TF binding",
|
|
|
|
|
|
"发育": "Development",
|
|
|
|
|
|
"其他": "Other",
|
|
|
|
|
|
}
|
|
|
|
|
|
categories: List[str] = []
|
|
|
|
|
|
if named_categories:
|
|
|
|
|
|
raw_categories = re.split(r"[,,、/]", named_categories)
|
|
|
|
|
|
for item in raw_categories:
|
|
|
|
|
|
key = item.strip().lower()
|
|
|
|
|
|
if key in category_aliases:
|
|
|
|
|
|
categories.append(category_aliases[key])
|
|
|
|
|
|
elif item.strip() in known_categories:
|
|
|
|
|
|
categories.append(item.strip())
|
|
|
|
|
|
for alias, category in category_aliases.items():
|
|
|
|
|
|
if alias in lower and category not in categories:
|
|
|
|
|
|
categories.append(category)
|
|
|
|
|
|
|
|
|
|
|
|
elements: List[str] = []
|
|
|
|
|
|
if named_elements:
|
|
|
|
|
|
elements.extend([item.strip() for item in re.split(r"[,,、/]", named_elements) if item.strip()])
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Prefer the phrase after "只看/只绘制/只画" for common PlantCARE element symbols.
|
|
|
|
|
|
element_region = text
|
|
|
|
|
|
match = re.search(r"(?:只看|只绘制|只画|元件|elements?)\s*([^。;;\n]+)", text, flags=re.IGNORECASE)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
element_region = match.group(1)
|
|
|
|
|
|
candidates = re.findall(r"\b[A-Za-z][A-Za-z0-9~-]{1,30}\b", element_region)
|
|
|
|
|
|
excluded = {
|
|
|
|
|
|
"Ft", "At", "Os", "Ls", "Zm", "Sl", "Lc", "Dl", "Ppy", "Py", "Cp", "Vv", "Lg",
|
|
|
|
|
|
"FPKM", "TPM", "DIY", "PlantCARE", "promoter", "plot", "heatmap", "barplot", "exact", "contains", "regex",
|
|
|
|
|
|
"position", "distribution", "Stress", "response", "Hormone", "signaling", "Light", "Core", "TF", "binding", "Development", "Other",
|
|
|
|
|
|
}
|
|
|
|
|
|
for candidate in candidates:
|
|
|
|
|
|
if candidate not in excluded and candidate.lower() not in {x.lower() for x in excluded}:
|
|
|
|
|
|
elements.append(candidate)
|
|
|
|
|
|
|
|
|
|
|
|
if elements:
|
|
|
|
|
|
params["promoterElements"] = ",".join(_unique_keep_order(elements))
|
|
|
|
|
|
if categories:
|
|
|
|
|
|
params["promoterCategories"] = ",".join(_unique_keep_order(categories))
|
|
|
|
|
|
|
|
|
|
|
|
match_value = (named_match or "").lower()
|
|
|
|
|
|
if "contains" in match_value or "包含" in lower or "关键词" in lower:
|
|
|
|
|
|
params["promoterMatchMode"] = "contains"
|
|
|
|
|
|
elif "regex" in match_value or "正则" in lower:
|
|
|
|
|
|
params["promoterMatchMode"] = "regex"
|
|
|
|
|
|
else:
|
|
|
|
|
|
params["promoterMatchMode"] = "exact"
|
|
|
|
|
|
|
|
|
|
|
|
modes: List[str] = []
|
|
|
|
|
|
mode_text = f"{named_modes} {lower}"
|
|
|
|
|
|
if "position_distribution" in mode_text or "位置分布" in lower or "位置" in lower:
|
|
|
|
|
|
modes.append("position_distribution")
|
|
|
|
|
|
if "heatmap" in mode_text or "热图" in lower:
|
|
|
|
|
|
modes.append("heatmap")
|
|
|
|
|
|
if "barplot" in mode_text or "柱状" in lower or "条形" in lower:
|
|
|
|
|
|
modes.append("barplot")
|
|
|
|
|
|
if not modes:
|
|
|
|
|
|
modes = ["position_distribution", "heatmap"]
|
|
|
|
|
|
params["promoterPlotModes"] = ",".join(_unique_keep_order(modes))
|
|
|
|
|
|
|
|
|
|
|
|
named_color_lower = named_color.lower().strip()
|
|
|
|
|
|
if named_color_lower:
|
|
|
|
|
|
if named_color_lower in {"element", "elements", "元件"}:
|
|
|
|
|
|
params["promoterColorBy"] = "element"
|
|
|
|
|
|
elif named_color_lower in {"category", "categories", "类别", "大类"}:
|
|
|
|
|
|
params["promoterColorBy"] = "category"
|
|
|
|
|
|
else:
|
|
|
|
|
|
params["promoterColorBy"] = "auto"
|
|
|
|
|
|
elif "按元件" in lower or "元件着色" in lower or "元件颜色" in lower or "color by element" in lower:
|
|
|
|
|
|
params["promoterColorBy"] = "element"
|
|
|
|
|
|
elif "按类别" in lower or "类别着色" in lower or "大类着色" in lower or "color by category" in lower:
|
|
|
|
|
|
params["promoterColorBy"] = "category"
|
|
|
|
|
|
|
|
|
|
|
|
top_match = re.search(r"(?:top\s*n|top|前)\s*[:=:]?\s*(\d{1,3})", lower, flags=re.IGNORECASE)
|
|
|
|
|
|
if top_match:
|
|
|
|
|
|
params["promoterTopN"] = int(top_match.group(1))
|
|
|
|
|
|
if named_label:
|
|
|
|
|
|
params["promoterOutputLabel"] = re.sub(r"[^A-Za-z0-9_.-]+", "_", named_label).strip("._-")
|
|
|
|
|
|
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WGS_STAGE_PHRASES = (
|
|
|
|
|
|
("wgs_maf_missing_filter", ("wgs maf缺失率过滤", "wgs maf missingness filter")),
|
|
|
|
|
|
("wgs_impute", ("wgs基因型填充", "wgs genotype imputation")),
|
|
|
|
|
|
("wgs_variant_filter", ("wgs变异过滤", "wgs variant filtering")),
|
|
|
|
|
|
("wgs_variant_select", ("wgs变异选择", "wgs variant selection")),
|
|
|
|
|
|
("wgs_joint_genotyping", ("wgs联合分型", "wgs joint genotyping")),
|
|
|
|
|
|
("wgs_gvcf_combine", ("wgs gvcf合并", "wgs gvcf combine")),
|
|
|
|
|
|
("wgs_haplotype_calling", ("wgs单样本变异检测", "wgs haplotype calling")),
|
|
|
|
|
|
("wgs_bam_qc", ("wgs bam质控", "wgs bam qc")),
|
|
|
|
|
|
("wgs_markdup", ("wgs去重复", "wgs mark duplicates")),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _wgs_intent(stage: str) -> Dict:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"gene_family": "",
|
|
|
|
|
|
"species": "",
|
|
|
|
|
|
"stages": [stage],
|
|
|
|
|
|
"primary_stage": stage,
|
|
|
|
|
|
"task_type": "analysis",
|
|
|
|
|
|
"task_kind": "pipeline",
|
|
|
|
|
|
"confidence": 0.98,
|
|
|
|
|
|
"need_clarify": False,
|
|
|
|
|
|
"clarify_type": None,
|
|
|
|
|
|
"message": None,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_intent_from_query(query: str) -> Dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
从查询中解析意图 - 硬编码规则(方案B)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
query: 用户自然语言查询
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
{
|
|
|
|
|
|
"gene_family": 基因家族名称,
|
|
|
|
|
|
"species": 物种代码,
|
|
|
|
|
|
"stages": [分析阶段列表],
|
|
|
|
|
|
"primary_stage": 主要分析阶段,
|
|
|
|
|
|
"task_type": "analysis" | "feasibility" | "report",
|
|
|
|
|
|
"confidence": 置信度,
|
|
|
|
|
|
"need_clarify": 是否需要澄清,
|
|
|
|
|
|
"clarify_type": 澄清类型,
|
|
|
|
|
|
"message": 提示消息
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
query_lower = query.lower()
|
|
|
|
|
|
query_without_space = re.sub(r"\s+", "", query_lower)
|
|
|
|
|
|
|
|
|
|
|
|
if any(token.replace(" ", "") in query_without_space for token in (
|
|
|
|
|
|
"wgs全流程", "vcf全流程", "wgs full pipeline", "vcf full pipeline"
|
|
|
|
|
|
)):
|
|
|
|
|
|
return _wgs_intent("wgs_maf_missing_filter")
|
|
|
|
|
|
for stage, phrases in WGS_STAGE_PHRASES:
|
|
|
|
|
|
if any(phrase.replace(" ", "") in query_without_space for phrase in phrases):
|
|
|
|
|
|
return _wgs_intent(stage)
|
|
|
|
|
|
if any(token in query_lower for token in ("wgs align", "wgs_align", "wgs alignment", "wgs序列比对", "wgs比对")):
|
|
|
|
|
|
return {"gene_family": "", "species": "", "stages": ["wgs_align"], "primary_stage": "wgs_align", "task_type": "analysis", "task_kind": "pipeline", "confidence": 0.98, "need_clarify": False, "clarify_type": None, "message": None}
|
|
|
|
|
|
if any(token in query_lower for token in ("wgs index", "wgs_index", "reference index", "参考索引", "参考基因组索引")):
|
|
|
|
|
|
return {"gene_family": "", "species": "", "stages": ["wgs_index"], "primary_stage": "wgs_index", "task_type": "analysis", "task_kind": "pipeline", "confidence": 0.98, "need_clarify": False, "clarify_type": None, "message": None}
|
|
|
|
|
|
if any(token in query_lower for token in ("wgs", "fastp", "全基因组测序", "wgs质控", "测序质控")):
|
|
|
|
|
|
return {"gene_family": "", "species": "", "stages": ["wgs_qc"], "primary_stage": "wgs_qc", "task_type": "analysis", "task_kind": "pipeline", "confidence": 0.98, "need_clarify": False, "clarify_type": None, "message": None}
|
|
|
|
|
|
|
|
|
|
|
|
population = resolve_population_intent(query)
|
|
|
|
|
|
if population:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"gene_family": "",
|
|
|
|
|
|
"species": "",
|
|
|
|
|
|
"stages": population["stages"],
|
|
|
|
|
|
"primary_stage": population["target"],
|
|
|
|
|
|
"task_type": "analysis",
|
|
|
|
|
|
"task_kind": "pipeline",
|
|
|
|
|
|
"confidence": 0.98,
|
|
|
|
|
|
"need_clarify": False,
|
|
|
|
|
|
"clarify_type": None,
|
|
|
|
|
|
"message": None,
|
|
|
|
|
|
"prefix": population["prefix"],
|
|
|
|
|
|
"params": population["params"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
gwas = resolve_gwas_intent(query)
|
|
|
|
|
|
if gwas:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"gene_family": "",
|
|
|
|
|
|
"species": "",
|
|
|
|
|
|
"stages": gwas["stages"],
|
|
|
|
|
|
"primary_stage": gwas["target"],
|
|
|
|
|
|
"task_type": "analysis",
|
|
|
|
|
|
"task_kind": "pipeline",
|
|
|
|
|
|
"confidence": 0.98,
|
|
|
|
|
|
"need_clarify": False,
|
|
|
|
|
|
"clarify_type": None,
|
|
|
|
|
|
"message": None,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 提取基因家族
|
|
|
|
|
|
gene_family = _extract_gene_family(query, query_lower)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取物种
|
|
|
|
|
|
species = _extract_species(query, query_lower)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取分析阶段
|
|
|
|
|
|
stages = _extract_stages(query, query_lower)
|
|
|
|
|
|
|
|
|
|
|
|
ncbi_params = parse_ncbi_fetch_request(query)
|
|
|
|
|
|
sequence_extract_params = {} if ncbi_params else parse_sequence_extract_request(query)
|
|
|
|
|
|
msa_region_params = {} if ncbi_params or sequence_extract_params else parse_msa_region_plot_request(query)
|
|
|
|
|
|
promoter_plot_params = {} if ncbi_params or sequence_extract_params or msa_region_params else parse_promoter_plot_revise_request(query)
|
|
|
|
|
|
if sequence_extract_params:
|
|
|
|
|
|
stages = ["sequence_extract_by_id"]
|
|
|
|
|
|
if ncbi_params:
|
|
|
|
|
|
stages = ["ncbi_fetch_sequences"]
|
|
|
|
|
|
if msa_region_params:
|
|
|
|
|
|
stages = ["msa_region_plot"]
|
|
|
|
|
|
if promoter_plot_params:
|
|
|
|
|
|
stages = ["promoter_plot_revise"]
|
|
|
|
|
|
|
|
|
|
|
|
# 确定任务类型
|
|
|
|
|
|
task_type = _determine_task_type(query, query_lower)
|
|
|
|
|
|
|
|
|
|
|
|
# 方案B: 处理不明确分析意图
|
|
|
|
|
|
result = _handle_stage_resolution(query, query_lower, stages)
|
|
|
|
|
|
|
|
|
|
|
|
parsed = {
|
|
|
|
|
|
"gene_family": gene_family or "",
|
|
|
|
|
|
"species": ncbi_params.get("ncbiSpecies") if ncbi_params.get("ncbiSpecies") else (species or ""),
|
|
|
|
|
|
"stages": result["stages"],
|
|
|
|
|
|
"primary_stage": result["stages"][0] if result["stages"] else None,
|
|
|
|
|
|
"task_type": task_type,
|
|
|
|
|
|
"task_kind": "pipeline",
|
|
|
|
|
|
"confidence": result.get("confidence", 0.95 if stages else 0.7),
|
|
|
|
|
|
"need_clarify": result.get("need_clarify", False),
|
|
|
|
|
|
"clarify_type": result.get("clarify_type"),
|
|
|
|
|
|
"message": result.get("message")
|
|
|
|
|
|
}
|
|
|
|
|
|
if ncbi_params:
|
|
|
|
|
|
parsed["params"] = ncbi_params
|
|
|
|
|
|
parsed["prefix"] = ncbi_params.get("prefix", "")
|
|
|
|
|
|
parsed["gene_family"] = "NCBI.fetch"
|
|
|
|
|
|
parsed["task_kind"] = "utility"
|
|
|
|
|
|
parsed["utility_tool"] = "ncbi_fetch_sequences"
|
|
|
|
|
|
parsed["message"] = _build_ncbi_message(ncbi_params)
|
|
|
|
|
|
if sequence_extract_params:
|
|
|
|
|
|
parsed["params"] = sequence_extract_params
|
|
|
|
|
|
parsed["prefix"] = sequence_extract_params.get("prefix", "")
|
|
|
|
|
|
parsed["gene_family"] = "Sequence.extract"
|
|
|
|
|
|
parsed["task_kind"] = "utility"
|
|
|
|
|
|
parsed["utility_tool"] = "fasta_header_clean" if sequence_extract_params.get("sequenceExtractCleanHeaders") else "sequence_extract_by_id"
|
|
|
|
|
|
parsed["message"] = _build_sequence_extract_message(sequence_extract_params)
|
|
|
|
|
|
if msa_region_params:
|
|
|
|
|
|
parsed["params"] = msa_region_params
|
|
|
|
|
|
parsed["prefix"] = msa_region_params.get("prefix", "")
|
|
|
|
|
|
parsed["gene_family"] = "MSA.region"
|
|
|
|
|
|
parsed["task_kind"] = "utility"
|
|
|
|
|
|
parsed["utility_tool"] = "msa_region_plot"
|
|
|
|
|
|
parsed["message"] = _build_msa_region_plot_message(msa_region_params)
|
|
|
|
|
|
if promoter_plot_params:
|
|
|
|
|
|
parsed["params"] = promoter_plot_params
|
|
|
|
|
|
parsed["prefix"] = promoter_plot_params.get("prefix", "")
|
|
|
|
|
|
parsed["gene_family"] = "Promoter.custom"
|
|
|
|
|
|
parsed["task_kind"] = "utility"
|
|
|
|
|
|
parsed["utility_tool"] = "promoter_plot_revise"
|
|
|
|
|
|
parsed["message"] = "我理解你想基于已有 PlantCARE/promoter 结果进行启动子自定义筛选绘图,不重新跑启动子预测。"
|
|
|
|
|
|
if parsed["primary_stage"] in UTILITY_KEYWORD_STAGES:
|
|
|
|
|
|
parsed["task_kind"] = "utility"
|
|
|
|
|
|
parsed["utility_tool"] = parsed.get("utility_tool") or parsed["primary_stage"]
|
|
|
|
|
|
if parsed["primary_stage"] in REPAIR_KEYWORD_STAGES:
|
|
|
|
|
|
parsed["task_kind"] = "repair"
|
|
|
|
|
|
scope_selection = parse_gene_family_scope_selection(query)
|
|
|
|
|
|
if any(scope_selection.get(key) for key in ("targetSpecies", "referenceSpecies", "comparativeSpecies")):
|
|
|
|
|
|
parsed["target_species"] = scope_selection.get("targetSpecies", [])
|
|
|
|
|
|
parsed["reference_species"] = scope_selection.get("referenceSpecies", [])
|
|
|
|
|
|
parsed["comparative_species"] = scope_selection.get("comparativeSpecies", [])
|
|
|
|
|
|
parsed["scopeMode"] = scope_selection.get("scopeMode", "target_only")
|
|
|
|
|
|
targets = parsed["target_species"]
|
|
|
|
|
|
if targets:
|
|
|
|
|
|
parsed["species"] = targets[0] if len(targets) == 1 else "Multi"
|
|
|
|
|
|
params = parsed.setdefault("params", {})
|
|
|
|
|
|
params.update(scope_selection)
|
|
|
|
|
|
if parsed.get("gene_family") and parsed.get("species") and parsed.get("task_kind") != "utility":
|
|
|
|
|
|
parsed["prefix"] = f"{parsed['species']}.{parsed['gene_family']}"
|
|
|
|
|
|
return parsed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_msa_region_plot_request(query: str) -> Dict:
|
|
|
|
|
|
"""Extract parameters for drawing a selected MSA alignment region."""
|
|
|
|
|
|
text = str(query or "").strip()
|
|
|
|
|
|
lower = text.lower()
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
has_msa = any(kw in lower for kw in ["msa", "alignment", "aligned", "多序列比对", "比对"])
|
|
|
|
|
|
has_region_plot = any(kw in lower for kw in [
|
|
|
|
|
|
"区间", "区域", "位置", "位点", "列", "范围", "片段", "局部", "重画", "二次绘图",
|
|
|
|
|
|
"绘图", "画图", "出图", "plot", "figure", "region", "window", "columns",
|
|
|
|
|
|
"保守区间", "保守位置", "保守窗口",
|
|
|
|
|
|
])
|
|
|
|
|
|
if not (has_msa and has_region_plot):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
params: Dict[str, object] = {}
|
|
|
|
|
|
prefix_match = re.search(r"\b[A-Za-z][A-Za-z0-9_.-]*\.[A-Za-z0-9_.-]+\b", text)
|
|
|
|
|
|
if prefix_match:
|
|
|
|
|
|
params["prefix"] = prefix_match.group(0).strip(".,;,;")
|
|
|
|
|
|
|
|
|
|
|
|
range_match = re.search(
|
|
|
|
|
|
r"(?:从|位置|位点|列|columns?|cols?|start)?\s*(\d{1,6})\s*(?:-|~|–|—|到|至|:|,|,)\s*(?:位置|位点|列|end)?\s*(\d{1,6})",
|
|
|
|
|
|
text,
|
|
|
|
|
|
flags=re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not range_match:
|
|
|
|
|
|
start_match = re.search(r"(?:start|起始|开始)\s*[:=]?\s*(\d{1,6})", text, flags=re.IGNORECASE)
|
|
|
|
|
|
end_match = re.search(r"(?:end|结束|终止)\s*[:=]?\s*(\d{1,6})", text, flags=re.IGNORECASE)
|
|
|
|
|
|
if start_match and end_match:
|
|
|
|
|
|
range_match = (start_match, end_match)
|
|
|
|
|
|
|
|
|
|
|
|
if range_match:
|
|
|
|
|
|
if isinstance(range_match, tuple):
|
|
|
|
|
|
start_col = int(range_match[0].group(1))
|
|
|
|
|
|
end_col = int(range_match[1].group(1))
|
|
|
|
|
|
else:
|
|
|
|
|
|
start_col = int(range_match.group(1))
|
|
|
|
|
|
end_col = int(range_match.group(2))
|
|
|
|
|
|
if start_col > end_col:
|
|
|
|
|
|
start_col, end_col = end_col, start_col
|
|
|
|
|
|
params["msaStartCol"] = start_col
|
|
|
|
|
|
params["msaEndCol"] = end_col
|
|
|
|
|
|
params["msaAutoConserved"] = False
|
|
|
|
|
|
|
|
|
|
|
|
auto_requested = any(kw in lower for kw in ["自动", "最保守", "保守窗口", "auto", "conserved window", "best window"])
|
|
|
|
|
|
if auto_requested and "msaStartCol" not in params:
|
|
|
|
|
|
params["msaAutoConserved"] = True
|
|
|
|
|
|
|
|
|
|
|
|
window_match = re.search(r"(?:窗口|window)\s*(?:长度|len|length)?\s*[:=]?\s*(\d{1,4})", text, flags=re.IGNORECASE)
|
|
|
|
|
|
if window_match:
|
|
|
|
|
|
params["msaWindowLen"] = max(1, int(window_match.group(1)))
|
|
|
|
|
|
|
|
|
|
|
|
nongap_match = re.search(r"(?:nongap|min[-_ ]?nongap|非缺口)\s*[:=]?\s*(0(?:\.\d+)?|1(?:\.0+)?)", text, flags=re.IGNORECASE)
|
|
|
|
|
|
if nongap_match:
|
|
|
|
|
|
params["msaMinNongap"] = float(nongap_match.group(1))
|
|
|
|
|
|
|
|
|
|
|
|
return params or {"msaAutoConserved": False}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_sequence_extract_request(query: str) -> Dict:
|
|
|
|
|
|
"""Extract parameters for local FASTA sequence extraction by IDs."""
|
|
|
|
|
|
text = _normalize_utility_query(query)
|
|
|
|
|
|
lower = text.lower()
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
has_header_clean = any(kw in lower for kw in [
|
|
|
|
|
|
"id处理", "处理id", "修订id", "简化id", "清理id", "id简化",
|
|
|
|
|
|
"去掉注释", "去除注释", "删除注释", "注释信息", "长注释",
|
|
|
|
|
|
"clean header", "simple header", "simplify header", "header clean",
|
|
|
|
|
|
"fasta header", "序列名", "序列头",
|
|
|
|
|
|
])
|
|
|
|
|
|
has_extract = any(kw in lower for kw in ["提取", "筛选", "extract", "subseq", "pull out", "取出"])
|
|
|
|
|
|
has_sequence = any(kw in lower for kw in ["序列", "fasta", "fa", "pep", "protein", "蛋白", "cds", "基因id", "id list", "id列表"])
|
|
|
|
|
|
if has_header_clean and any(kw in lower for kw in ["序列", "fasta", "fa", "pep", "protein", "蛋白", "cds", "家族"]):
|
|
|
|
|
|
paths = _extract_file_paths(text)
|
|
|
|
|
|
fasta_paths = [
|
|
|
|
|
|
path for path in paths
|
|
|
|
|
|
if re.search(r"\.(fa|fasta|faa|fna|ffn|pep|cds)(?:\.gz)?$", path.lower())
|
|
|
|
|
|
or any(x in path.lower() for x in ["pep", "protein", "cds", "fasta"])
|
|
|
|
|
|
]
|
|
|
|
|
|
seq_type = "auto"
|
|
|
|
|
|
if any(kw in lower for kw in ["pep", "protein", "蛋白"]):
|
|
|
|
|
|
seq_type = "pep"
|
|
|
|
|
|
elif "cds" in lower:
|
|
|
|
|
|
seq_type = "cds"
|
|
|
|
|
|
prefix = _extract_prefix(text) or _extract_family_prefix(text, lower)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"sequenceExtractFasta": ",".join(fasta_paths),
|
|
|
|
|
|
"sequenceExtractIdsFile": "",
|
|
|
|
|
|
"sequenceExtractIds": "",
|
|
|
|
|
|
"sequenceExtractSeqType": seq_type,
|
|
|
|
|
|
"sequenceExtractRenameMode": "simple",
|
|
|
|
|
|
"sequenceExtractCleanHeaders": True,
|
|
|
|
|
|
"prefix": prefix or "sequence_clean",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if not (has_extract and has_sequence):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
if "ncbi" in lower and any(kw in lower for kw in ["下载", "download"]):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
paths = _extract_file_paths(text)
|
|
|
|
|
|
fasta_paths = []
|
|
|
|
|
|
id_paths = []
|
|
|
|
|
|
for path in paths:
|
|
|
|
|
|
name = path.lower()
|
|
|
|
|
|
if re.search(r"\.(fa|fasta|faa|fna|ffn|pep|cds)(?:\.gz)?$", name) or any(x in name for x in ["pep", "protein", "cds", "fasta"]):
|
|
|
|
|
|
fasta_paths.append(path)
|
|
|
|
|
|
elif re.search(r"\.(txt|tsv|csv|list|ids)$", name):
|
|
|
|
|
|
id_paths.append(path)
|
|
|
|
|
|
|
|
|
|
|
|
ids_ref = _extract_ids_list_reference(text)
|
|
|
|
|
|
if ids_ref and ids_ref not in id_paths:
|
|
|
|
|
|
id_paths.append(ids_ref)
|
|
|
|
|
|
|
|
|
|
|
|
inline_ids = _extract_sequence_ids(text)
|
|
|
|
|
|
if not inline_ids and not id_paths:
|
|
|
|
|
|
# If the user provided one non-FASTA path in an ID-list context, treat it as ID file.
|
|
|
|
|
|
for path in paths:
|
|
|
|
|
|
if path not in fasta_paths:
|
|
|
|
|
|
id_paths.append(path)
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
if not inline_ids and not id_paths:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
seq_type = "auto"
|
|
|
|
|
|
if any(kw in lower for kw in ["pep", "protein", "蛋白"]):
|
|
|
|
|
|
seq_type = "pep"
|
|
|
|
|
|
elif "cds" in lower:
|
|
|
|
|
|
seq_type = "cds"
|
|
|
|
|
|
|
|
|
|
|
|
rename_mode = "keep"
|
|
|
|
|
|
if any(kw in lower for kw in ["重命名", "重新命名", "rename", "命名为查询", "按查询命名"]):
|
|
|
|
|
|
rename_mode = "query"
|
|
|
|
|
|
elif any(kw in lower for kw in ["简化命名", "simple header", "简化header"]):
|
|
|
|
|
|
rename_mode = "simple"
|
|
|
|
|
|
|
|
|
|
|
|
prefix = _extract_prefix(text)
|
|
|
|
|
|
if not prefix:
|
|
|
|
|
|
source = id_paths[0] if id_paths else (fasta_paths[0] if fasta_paths else "")
|
|
|
|
|
|
prefix = _prefix_from_path(source) if source else "sequence_extract"
|
|
|
|
|
|
|
|
|
|
|
|
params: Dict = {
|
|
|
|
|
|
"sequenceExtractFasta": ",".join(fasta_paths),
|
|
|
|
|
|
"sequenceExtractIdsFile": id_paths[0] if id_paths else "",
|
|
|
|
|
|
"sequenceExtractIds": " ".join(inline_ids),
|
|
|
|
|
|
"sequenceExtractSeqType": seq_type,
|
|
|
|
|
|
"sequenceExtractRenameMode": rename_mode,
|
|
|
|
|
|
"prefix": prefix,
|
|
|
|
|
|
}
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_utility_query(text: str) -> str:
|
|
|
|
|
|
normalized = re.sub(r"\s+", " ", str(text or "").strip())
|
|
|
|
|
|
return re.sub(
|
|
|
|
|
|
r"([A-Za-z0-9][A-Za-z0-9_.-]*)\s+\.(ids?|list|txt|tsv|csv)\b",
|
|
|
|
|
|
r"\1.\2",
|
|
|
|
|
|
normalized,
|
|
|
|
|
|
flags=re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_ids_list_reference(text: str) -> Optional[str]:
|
|
|
|
|
|
normalized = _normalize_utility_query(text)
|
|
|
|
|
|
path = _extract_file_path(normalized)
|
|
|
|
|
|
if path:
|
|
|
|
|
|
return path
|
|
|
|
|
|
match = re.search(
|
|
|
|
|
|
r"(?<![A-Za-z0-9_.-])([A-Za-z0-9][A-Za-z0-9_.-]*\.(?:ids?|list|txt|tsv|csv))\b",
|
|
|
|
|
|
normalized,
|
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
|
)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
return match.group(1)
|
|
|
|
|
|
prefix_match = re.search(r"\b([A-Za-z]{1,6}\.[A-Za-z0-9_-]+)\b", normalized)
|
|
|
|
|
|
if prefix_match and re.search(r"(?:\.\s*)?(?:ids?|list|txt|tsv|csv)\b", normalized, re.IGNORECASE):
|
|
|
|
|
|
return f"{prefix_match.group(1)}.ids"
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _species_code_from_prefix_like(value: str) -> Optional[str]:
|
|
|
|
|
|
token = str(value or "").strip().split("/")[-1]
|
|
|
|
|
|
match = re.match(r"([A-Za-z]{1,6})(?:[._-]|$)", token)
|
|
|
|
|
|
if not match:
|
|
|
|
|
|
return None
|
|
|
|
|
|
raw = match.group(1).lower()
|
|
|
|
|
|
canonical = {code.lower(): code for code in SPECIES_KEYWORDS}
|
|
|
|
|
|
return canonical.get(raw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_ncbi_fetch_request(query: str) -> Dict:
|
|
|
|
|
|
"""Extract conservative NCBI sequence-download parameters from natural language."""
|
|
|
|
|
|
text = _normalize_utility_query(query)
|
|
|
|
|
|
lower = text.lower()
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
result_view_context = (
|
|
|
|
|
|
any(kw in lower or kw in text for kw in ["下载好的", "已下载", "已经下载", "下载结果", "下载产物", "已有结果"])
|
|
|
|
|
|
and any(kw in lower or kw in text for kw in ["展示", "显示", "列出", "查看", "列表", "明细"])
|
|
|
|
|
|
and any(kw in lower or kw in text for kw in ["基因id", "gene id", "gene ids", "id列表", "ids", "id"])
|
|
|
|
|
|
)
|
|
|
|
|
|
if result_view_context:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
existing_result_file_context = (
|
|
|
|
|
|
bool(re.search(r"[A-Za-z0-9][A-Za-z0-9_.-]*\.(?:fa|fasta|faa|fna|ffn|pep|cds|txt|tsv|csv)(?=\s|$|[,,。;;的])", text, flags=re.IGNORECASE))
|
|
|
|
|
|
and any(kw in lower or kw in text for kw in [
|
|
|
|
|
|
"过滤", "只保留", "提取", "抽取", "查看", "展示", "显示", "列出", "预览",
|
|
|
|
|
|
"前几行", "前几列", "表头", "列名", "id列", "蛋白id", "蛋白 id",
|
|
|
|
|
|
"filter", "extract", "preview", "head",
|
|
|
|
|
|
])
|
|
|
|
|
|
and not any(kw in lower or kw in text for kw in ["下载", "获取", "download"])
|
|
|
|
|
|
and not re.search(r"(?<!ncbi_)fetch\b", lower)
|
|
|
|
|
|
)
|
|
|
|
|
|
if existing_result_file_context:
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
has_ncbi = "ncbi" in lower
|
|
|
|
|
|
has_sequence_download = any(kw in lower for kw in [
|
|
|
|
|
|
"下载", "获取", "fetch", "download", "序列", "protein", "蛋白", "pep", "cds", "geneid", "gene id",
|
|
|
|
|
|
])
|
|
|
|
|
|
has_gene_list = any(kw in lower for kw in [
|
|
|
|
|
|
"基因list", "基因列表", "基因id", "基因ids", "gene list", "gene_list", "gene id", "gene ids", "id列表", "id list",
|
|
|
|
|
|
"id文件", "ids文件",
|
|
|
|
|
|
]) or bool(re.search(r"\.(?:ids?|list|txt|tsv|csv)(?=\s|$|[的,,。;;]|下载|获取|fetch|download|提取|筛选)", lower))
|
|
|
|
|
|
has_path = bool(_extract_file_path(text))
|
|
|
|
|
|
has_local_extract = any(kw in lower for kw in ["提取", "筛选", "extract", "subseq", "pull out", "取出"])
|
|
|
|
|
|
has_explicit_download = any(kw in lower for kw in ["下载", "download", "fetch", "ncbi"])
|
|
|
|
|
|
if has_local_extract and not has_explicit_download and (has_gene_list or has_path):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
if not (has_ncbi or (has_sequence_download and (has_gene_list or has_path))):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
params: Dict = {
|
|
|
|
|
|
"ncbiMode": "gene",
|
|
|
|
|
|
"ncbiIdType": "auto",
|
|
|
|
|
|
"ncbiSeqTypes": "both",
|
|
|
|
|
|
"ncbiTranscriptPolicy": "refseq_preferred",
|
|
|
|
|
|
"ncbiAllowFamilyDownload": False,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
path = _extract_ids_list_reference(text)
|
|
|
|
|
|
if path:
|
|
|
|
|
|
params["ncbiGeneIds"] = path
|
|
|
|
|
|
|
|
|
|
|
|
inferred_code = _species_code_from_prefix_like(path or params.get("prefix", ""))
|
|
|
|
|
|
species = _extract_latin_species(text) or _extract_species_scientific_name(text, lower)
|
|
|
|
|
|
if not species and inferred_code:
|
|
|
|
|
|
species = SPECIES_TO_LATIN.get(inferred_code) or ""
|
|
|
|
|
|
if species:
|
|
|
|
|
|
params["ncbiSpecies"] = species
|
|
|
|
|
|
|
|
|
|
|
|
taxid = _extract_taxid(text)
|
|
|
|
|
|
if not taxid and inferred_code:
|
|
|
|
|
|
taxid = SPECIES_TO_TAXID.get(inferred_code) or ""
|
|
|
|
|
|
if taxid:
|
|
|
|
|
|
params["ncbiTaxid"] = taxid
|
|
|
|
|
|
|
|
|
|
|
|
prefix = _extract_prefix(text)
|
|
|
|
|
|
if not prefix and path:
|
|
|
|
|
|
prefix = _prefix_from_path(path)
|
|
|
|
|
|
if prefix:
|
|
|
|
|
|
params["prefix"] = prefix
|
|
|
|
|
|
|
|
|
|
|
|
email = _extract_email(text)
|
|
|
|
|
|
if email:
|
|
|
|
|
|
params["ncbiEmail"] = email
|
|
|
|
|
|
|
|
|
|
|
|
if re.search(r"\b(geneid|gene id|ncbi gene)\b", lower) or "geneid" in lower:
|
|
|
|
|
|
params["ncbiIdType"] = "geneid"
|
|
|
|
|
|
elif any(kw in lower for kw in ["locus", "位点", "locus tag"]):
|
|
|
|
|
|
params["ncbiIdType"] = "locus_tag"
|
|
|
|
|
|
elif any(kw in lower for kw in ["symbol", "基因名", "基因名称"]):
|
|
|
|
|
|
params["ncbiIdType"] = "symbol"
|
|
|
|
|
|
|
|
|
|
|
|
if any(kw in lower for kw in ["家族", "family"]) and not path:
|
|
|
|
|
|
params["ncbiMode"] = "family"
|
|
|
|
|
|
params["ncbiAllowFamilyDownload"] = any(kw in lower for kw in ["确认下载", "允许批量下载", "allow-family-download", "直接下载全部"])
|
|
|
|
|
|
|
|
|
|
|
|
wants_protein = any(kw in lower for kw in ["protein", "蛋白", "pep"])
|
|
|
|
|
|
wants_cds = "cds" in lower
|
|
|
|
|
|
if wants_protein and not wants_cds:
|
|
|
|
|
|
params["ncbiSeqTypes"] = "protein"
|
|
|
|
|
|
elif wants_cds and not wants_protein:
|
|
|
|
|
|
params["ncbiSeqTypes"] = "cds"
|
|
|
|
|
|
else:
|
|
|
|
|
|
params["ncbiSeqTypes"] = "both"
|
|
|
|
|
|
|
|
|
|
|
|
query_value = _extract_gene_query(text)
|
|
|
|
|
|
if query_value and not path:
|
|
|
|
|
|
params["ncbiQuery"] = query_value
|
|
|
|
|
|
|
|
|
|
|
|
if not (params.get("ncbiGeneIds") or params.get("ncbiQuery")):
|
|
|
|
|
|
return {}
|
|
|
|
|
|
return params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_file_path(text: str) -> Optional[str]:
|
|
|
|
|
|
patterns = [
|
|
|
|
|
|
r"(?:基因\s*list|基因列表|基因\s*id|基因\s*ids|gene\s*list|gene_list|gene\s*id|gene\s*ids|id列表|id\s*list|ids?|id文件|ids文件|文件|file)\s*(?:放在|在|位于|是|为|:|:)?\s*([/~][^\s,,;;。]+)",
|
|
|
|
|
|
r"([/~][^\s,,;;。]+(?:\.txt|\.tsv|\.csv|\.list|\.ids?))",
|
|
|
|
|
|
r"(?:文件名|文件|file|基因\s*list|基因列表|id列表|id\s*list|id文件|ids文件)\s*(?:叫|名为|是|为|:|:)?\s*([A-Za-z0-9][A-Za-z0-9_.-]*\.(?:txt|tsv|csv|list|ids?))",
|
|
|
|
|
|
r"(?<![A-Za-z0-9_.-])([A-Za-z0-9][A-Za-z0-9_.-]*\.(?:txt|tsv|csv|list|ids?))(?=\s|$|[的,,。;;]|下载|获取|fetch|download|提取|筛选)",
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in patterns:
|
|
|
|
|
|
match = re.search(pattern, text, re.IGNORECASE)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
return match.group(1).strip().strip("'\"")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_file_paths(text: str) -> List[str]:
|
|
|
|
|
|
paths = []
|
|
|
|
|
|
for match in re.finditer(r"([/~][^\s,,;;。'\"<>]+)", text):
|
|
|
|
|
|
path = match.group(1).strip().rstrip(")")
|
|
|
|
|
|
if path not in paths:
|
|
|
|
|
|
paths.append(path)
|
|
|
|
|
|
for match in re.finditer(
|
|
|
|
|
|
r"(?<![A-Za-z0-9_.-])([A-Za-z0-9][A-Za-z0-9_.-]*\.(?:fa|fasta|faa|fna|ffn|pep|cds|txt|tsv|csv|list|ids?)(?:\.gz)?)(?=\s|$|[的,,。;;])",
|
|
|
|
|
|
text,
|
|
|
|
|
|
re.IGNORECASE,
|
|
|
|
|
|
):
|
|
|
|
|
|
path = match.group(1).strip().rstrip(")")
|
|
|
|
|
|
if path not in paths:
|
|
|
|
|
|
paths.append(path)
|
|
|
|
|
|
return paths
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_sequence_ids(text: str) -> List[str]:
|
|
|
|
|
|
cleaned = re.sub(r"[/~][^\s,,;;。'\"<>]+", " ", text)
|
|
|
|
|
|
ids = []
|
|
|
|
|
|
patterns = [
|
|
|
|
|
|
r"\b[A-Za-z]{1,4}\d+[gGtT]\d+(?:\.\d+)?\b",
|
|
|
|
|
|
r"\b[A-Za-z]{1,6}_\d+(?:\.\d+)?\b",
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in patterns:
|
|
|
|
|
|
for match in re.finditer(pattern, cleaned):
|
|
|
|
|
|
value = match.group(0)
|
|
|
|
|
|
if value not in ids:
|
|
|
|
|
|
ids.append(value)
|
|
|
|
|
|
explicit = re.search(
|
|
|
|
|
|
r"(?:基因\s*id|gene\s*id|(?<![A-Za-z0-9_.-])id(?!s\b))\s*(?:是|为|:|:)?\s*([A-Za-z0-9_.;,\s-]+)",
|
|
|
|
|
|
cleaned,
|
|
|
|
|
|
re.I,
|
|
|
|
|
|
)
|
|
|
|
|
|
if explicit:
|
|
|
|
|
|
for token in re.split(r"[\s,;,;]+", explicit.group(1)):
|
|
|
|
|
|
token = token.strip()
|
|
|
|
|
|
if token and token.lower() not in {"list", "file", "pep", "protein", "cds"} and token not in ids:
|
|
|
|
|
|
ids.append(token)
|
|
|
|
|
|
return ids
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _prefix_from_path(path: str) -> str:
|
|
|
|
|
|
name = str(path).rstrip("/").split("/")[-1]
|
|
|
|
|
|
name = re.sub(r"\.(txt|tsv|csv|list|ids?)$", "", name, flags=re.IGNORECASE)
|
|
|
|
|
|
name = re.sub(r"[^A-Za-z0-9_.-]+", "_", name).strip("._")
|
|
|
|
|
|
return name or "NCBI.fetch"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_latin_species(text: str) -> Optional[str]:
|
|
|
|
|
|
patterns = [
|
|
|
|
|
|
r"(?:物种|species)\s*(?:是|为|:|:)?\s*([A-Z][a-z]+(?:\s+[a-z][a-z0-9.-]+){1,3})",
|
|
|
|
|
|
r"\b([A-Z][a-z]+(?:\s+[a-z][a-z0-9.-]+){1,3})\b",
|
|
|
|
|
|
]
|
|
|
|
|
|
stop_words = {"GeneID", "NCBI"}
|
|
|
|
|
|
for pattern in patterns:
|
|
|
|
|
|
match = re.search(pattern, text)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
candidate = match.group(1).strip()
|
|
|
|
|
|
if candidate.split()[0] not in stop_words:
|
|
|
|
|
|
return candidate
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_species_scientific_name(text: str, lower: Optional[str] = None) -> Optional[str]:
|
|
|
|
|
|
lower = lower if lower is not None else text.lower()
|
|
|
|
|
|
for code, keywords in SPECIES_KEYWORDS.items():
|
|
|
|
|
|
if any(kw.lower() in lower for kw in keywords):
|
|
|
|
|
|
return SPECIES_TO_LATIN.get(code)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_taxid(text: str) -> Optional[str]:
|
|
|
|
|
|
match = re.search(r"(?:taxid|taxon|taxonomy)\s*(?:是|为|:|:)?\s*(\d+)", text, re.IGNORECASE)
|
|
|
|
|
|
return match.group(1) if match else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_prefix(text: str) -> Optional[str]:
|
|
|
|
|
|
match = re.search(r"(?:输出前缀|前缀|prefix)\s*(?:用|是|为|:|:)?\s*([A-Za-z0-9_.-]+)", text, re.IGNORECASE)
|
|
|
|
|
|
return match.group(1) if match else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_family_prefix(text: str, lower: str) -> Optional[str]:
|
|
|
|
|
|
species = None
|
|
|
|
|
|
for code, keywords in SPECIES_KEYWORDS.items():
|
|
|
|
|
|
if any(kw.lower() in lower for kw in keywords):
|
|
|
|
|
|
species = code
|
|
|
|
|
|
break
|
|
|
|
|
|
family = None
|
|
|
|
|
|
for item in COMMON_FAMILIES:
|
|
|
|
|
|
if item.lower() in lower or re.search(rf"(?<![A-Za-z0-9]){re.escape(item)}(?![A-Za-z0-9])", text, re.IGNORECASE):
|
|
|
|
|
|
family = item.upper() if item.isupper() else item
|
|
|
|
|
|
break
|
|
|
|
|
|
if species and family:
|
|
|
|
|
|
return f"{species}.{family}"
|
|
|
|
|
|
if family:
|
|
|
|
|
|
return family
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_email(text: str) -> Optional[str]:
|
|
|
|
|
|
match = re.search(r"[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}", text)
|
|
|
|
|
|
return match.group(0) if match else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_gene_query(text: str) -> Optional[str]:
|
|
|
|
|
|
patterns = [
|
|
|
|
|
|
r"(?:基因|gene)\s*(?:是|为|:|:)?\s*([A-Za-z0-9_.-]+)",
|
|
|
|
|
|
r"(?:下载|获取|fetch|download)\s*([A-Za-z0-9_.-]+)\s*(?:的)?(?:蛋白|protein|cds|序列)",
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in patterns:
|
|
|
|
|
|
match = re.search(pattern, text, re.IGNORECASE)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
value = match.group(1).strip()
|
|
|
|
|
|
if value.lower() not in {"list", "protein", "cds", "ncbi"}:
|
|
|
|
|
|
return value
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_ncbi_message(params: Dict) -> str:
|
|
|
|
|
|
source = params.get("ncbiGeneIds") or params.get("ncbiQuery") or "未指定"
|
|
|
|
|
|
species = params.get("ncbiTaxid") or params.get("ncbiSpecies") or "未指定"
|
|
|
|
|
|
return f"我理解你想从 NCBI 下载序列;输入为 {source},物种限制为 {species}。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_sequence_extract_message(params: Dict) -> str:
|
|
|
|
|
|
source = params.get("sequenceExtractFasta") or "自动从当前job查找FASTA"
|
|
|
|
|
|
if params.get("sequenceExtractCleanHeaders"):
|
|
|
|
|
|
return f"我理解你想清理FASTA序列ID/header;FASTA来源为 {source},输出将保留每条序列header的第一个ID。"
|
|
|
|
|
|
ids = params.get("sequenceExtractIdsFile") or params.get("sequenceExtractIds") or "未指定"
|
|
|
|
|
|
return f"我理解你想按ID从本地FASTA提取序列;FASTA来源为 {source},ID来源为 {ids}。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_msa_region_plot_message(params: Dict) -> str:
|
|
|
|
|
|
if params.get("msaAutoConserved"):
|
|
|
|
|
|
return f"我理解你想自动寻找 MSA 保守窗口并绘图;窗口长度为 {params.get('msaWindowLen', 60)}。"
|
|
|
|
|
|
start_col = params.get("msaStartCol")
|
|
|
|
|
|
end_col = params.get("msaEndCol")
|
|
|
|
|
|
if start_col and end_col:
|
|
|
|
|
|
return f"我理解你想对 MSA alignment 第 {start_col}-{end_col} 列进行局部绘图。"
|
|
|
|
|
|
return "我理解你想对 MSA 指定区间进行局部绘图;请在确认弹窗中填写 alignment 起止列。"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_stage_resolution(query: str, query_lower: str, stages: List[str]) -> Dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
方案B: 处理阶段解析逻辑
|
|
|
|
|
|
- 明确指定阶段 → 直接执行
|
|
|
|
|
|
- "基因家族分析" → 基础基因家族链路
|
|
|
|
|
|
- 有"分析"但不明确 → 引导澄清
|
|
|
|
|
|
- 完全无法识别 → 引导澄清
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 情况1: 明确指定了具体阶段 → 直接执行。
|
|
|
|
|
|
# 如果同时出现“完整/全流程”等词,则以基础基因家族链路为主体,
|
|
|
|
|
|
# 再追加用户明确点名的 Expression/KaKs/PPI/种间共线性等可选阶段。
|
|
|
|
|
|
if stages:
|
|
|
|
|
|
resolved_stages = list(dict.fromkeys([*CORE_STAGES, *stages])) if _is_complete_workflow_query(query_lower) else stages
|
|
|
|
|
|
return {
|
|
|
|
|
|
"stages": resolved_stages,
|
|
|
|
|
|
"need_clarify": False,
|
|
|
|
|
|
"confidence": 0.95
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 情况1.5: 泛化 RNA-seq 请求 → 默认走候选基因发现流程
|
|
|
|
|
|
# 例如“帮我执行rnaseq分析”“开始转录组分析”
|
|
|
|
|
|
if any(marker in query_lower for marker in RNASEQ_MARKERS):
|
|
|
|
|
|
if any(kw in query_lower for kw in ["分析", "执行", "运行", "开始", "流程", "pipeline"]):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"stages": ["rnaseq_de", "rnaseq_go_enrich", "rnaseq_kegg_enrich", "rnaseq_fgsea", "rnaseq_wgcna", "rnaseq_candidate_family"],
|
|
|
|
|
|
"need_clarify": False,
|
|
|
|
|
|
"confidence": 0.9,
|
|
|
|
|
|
"note": "generic_rnaseq_workflow"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 情况2: "基因家族分析" = 基础基因家族链路
|
|
|
|
|
|
if _is_complete_workflow_query(query_lower):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"stages": CORE_STAGES,
|
|
|
|
|
|
"need_clarify": False,
|
|
|
|
|
|
"confidence": 0.9,
|
|
|
|
|
|
"note": "完整流程"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 情况3: 有"分析"但不明确阶段 → 引导澄清
|
|
|
|
|
|
if "分析" in query or "analyze" in query_lower:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"stages": [],
|
|
|
|
|
|
"need_clarify": True,
|
|
|
|
|
|
"clarify_type": "choose_stages",
|
|
|
|
|
|
"confidence": 0.5,
|
|
|
|
|
|
"message": "请指定具体阶段,例如:鉴定、进化树、motif分析、domain分析等"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 情况4: 完全无法识别
|
|
|
|
|
|
return {
|
|
|
|
|
|
"stages": [],
|
|
|
|
|
|
"need_clarify": True,
|
|
|
|
|
|
"clarify_type": "unknown",
|
|
|
|
|
|
"confidence": 0.3,
|
|
|
|
|
|
"message": "无法理解意图,请使用标准格式:'分析 [阶段] [基因家族] [物种]'"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_complete_workflow_query(query_lower: str) -> bool:
|
|
|
|
|
|
"""检查是否是完整流程查询"""
|
|
|
|
|
|
for kw in COMPLETE_WORKFLOW_KEYWORDS:
|
|
|
|
|
|
if kw in query_lower:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_gene_family(query: str, query_lower: str) -> Optional[str]:
|
|
|
|
|
|
"""提取基因家族"""
|
|
|
|
|
|
# 优先匹配纯英文基因家族名(如 SPL, NAC, WRKY)
|
|
|
|
|
|
# 第一种模式:英文名 + 基因家族
|
|
|
|
|
|
pure_english_patterns = [
|
|
|
|
|
|
r'([A-Za-z][A-Za-z0-9]*)\s*基因家族', # SPL基因家族
|
|
|
|
|
|
r'基因家族\s*([A-Za-z][A-Za-z0-9]*)', # 基因家族SPL
|
|
|
|
|
|
]
|
|
|
|
|
|
species_codes = {code.lower() for code in SPECIES_KEYWORDS}
|
|
|
|
|
|
for pattern in pure_english_patterns:
|
|
|
|
|
|
match = re.search(pattern, query, re.IGNORECASE)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
raw_value = match.group(1)
|
|
|
|
|
|
value = raw_value.upper()
|
|
|
|
|
|
if raw_value.lower() in species_codes:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if value not in EXCLUDE_WORDS and value.lower() not in {w.lower() for w in EXCLUDE_WORDS}:
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
# 第二种模式:中英文混合(需要过滤)
|
|
|
|
|
|
mixed_patterns = [
|
|
|
|
|
|
r'(\w+)\s*基因家族',
|
|
|
|
|
|
r'基因家族\s*(\w+)',
|
|
|
|
|
|
]
|
|
|
|
|
|
for pattern in mixed_patterns:
|
|
|
|
|
|
match = re.search(pattern, query, re.IGNORECASE)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
extracted = match.group(1).upper()
|
|
|
|
|
|
# 排除中文请求词和包含中文的结果
|
|
|
|
|
|
if extracted not in EXCLUDE_WORDS and extracted.lower() not in {w.lower() for w in EXCLUDE_WORDS}:
|
|
|
|
|
|
# 如果提取结果包含中文,尝试提取其中的纯英文部分
|
|
|
|
|
|
english_only = re.sub(r'[^A-Za-z0-9]', '', extracted)
|
|
|
|
|
|
if english_only and english_only.lower() in species_codes:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if english_only and english_only not in EXCLUDE_WORDS:
|
|
|
|
|
|
return english_only
|
|
|
|
|
|
# 否则跳过,继续匹配下一个模式
|
|
|
|
|
|
|
|
|
|
|
|
# 检查常见基因家族
|
|
|
|
|
|
for fam in COMMON_FAMILIES:
|
|
|
|
|
|
if fam.lower() in query_lower:
|
|
|
|
|
|
return fam
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_species(query: str, query_lower: str) -> Optional[str]:
|
|
|
|
|
|
"""提取物种"""
|
|
|
|
|
|
for species, keywords in SPECIES_KEYWORDS.items():
|
|
|
|
|
|
for kw in keywords:
|
|
|
|
|
|
if kw in query_lower:
|
|
|
|
|
|
return species
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_stages(query: str, query_lower: str) -> List[str]:
|
|
|
|
|
|
"""提取分析阶段"""
|
|
|
|
|
|
stages = []
|
|
|
|
|
|
if is_gwas_fmt_intent(query):
|
|
|
|
|
|
stages.append("gwas_fmt")
|
|
|
|
|
|
matched_keywords = [] # 记录匹配到的关键词长度,用于优先级判断
|
|
|
|
|
|
has_rnaseq_marker = any(marker in query_lower for marker in RNASEQ_MARKERS)
|
|
|
|
|
|
extraction_context = any(kw in query_lower for kw in ["提取", "extract", "gffread", "筛选", "subseq"])
|
|
|
|
|
|
explicit_gene_family_context = any(kw in query_lower for kw in ["基因家族", "家族表达", "表达量", "热图", "heatmap"])
|
|
|
|
|
|
curated_member_context = (
|
|
|
|
|
|
any(kw in query_lower for kw in ["修订后的", "修订后", "人工修订", "curated", "edited"])
|
|
|
|
|
|
and any(kw in query_lower for kw in ["家族id", "成员id", "基因家族id", "member id", "members"])
|
|
|
|
|
|
)
|
|
|
|
|
|
if curated_member_context and any(kw in query_lower for kw in ["重新提取", "重建", "提取蛋白", "蛋白序列", "final_members"]):
|
|
|
|
|
|
stages.append("identify_refine")
|
|
|
|
|
|
if any(kw in query_lower for kw in ["确认亚家族", "亚家族确认", "确认亚家族标签", "人工确认亚家族", "subfamily confirm", "confirmed subfamily"]):
|
|
|
|
|
|
stages.append("subfamily_confirm")
|
|
|
|
|
|
rna_stage_hint = None
|
|
|
|
|
|
if any(kw in query_lower for kw in ["候选家族", "候选基因家族", "家族排名", "tf注释", "tf family", "tffamily", "pfam注释", "deg注释", "差异基因注释", "差异基因家族", "candidate family"]):
|
|
|
|
|
|
rna_stage_hint = "rnaseq_candidate_family"
|
|
|
|
|
|
elif any(kw in query_lower for kw in ["fgsea", "gsea", "排序富集", "基因集富集"]):
|
|
|
|
|
|
rna_stage_hint = "rnaseq_fgsea"
|
|
|
|
|
|
elif any(kw in query_lower for kw in ["wgcna", "共表达", "加权共表达", "模块性状"]):
|
|
|
|
|
|
rna_stage_hint = "rnaseq_wgcna"
|
|
|
|
|
|
elif any(kw in query_lower for kw in ["样本质量", "样本质量评估", "样本检测", "sample qc", "sample quality", "pca", "样本相关性", "样本聚类"]):
|
|
|
|
|
|
rna_stage_hint = "rnaseq_sample_qc"
|
|
|
|
|
|
elif any(kw in query_lower for kw in ["差异表达", "差异分析", "差异基因", "deg", "deseq2", "edger"]) and not explicit_gene_family_context:
|
|
|
|
|
|
rna_stage_hint = "rnaseq_de"
|
|
|
|
|
|
elif any(kw in query_lower for kw in ["质控", "qc", "fastqc", "测序质量"]):
|
|
|
|
|
|
rna_stage_hint = "rnaseq_qc"
|
|
|
|
|
|
elif any(kw in query_lower for kw in ["定量", "quant", "featurecounts", "count矩阵", "计数矩阵"]):
|
|
|
|
|
|
rna_stage_hint = "rnaseq_quant"
|
|
|
|
|
|
elif any(kw in query_lower for kw in ["比对", "align", "mapping", "hisat2", "star"]) and not any(kw in query_lower for kw in ["多序列", "msa", "multiple alignment"]):
|
|
|
|
|
|
rna_stage_hint = "rnaseq_align"
|
|
|
|
|
|
|
|
|
|
|
|
if rna_stage_hint:
|
|
|
|
|
|
stages.append(rna_stage_hint)
|
|
|
|
|
|
|
|
|
|
|
|
for stage, keywords in STAGE_KEYWORDS.items():
|
|
|
|
|
|
if stage in UTILITY_KEYWORD_STAGES or stage in REPAIR_KEYWORD_STAGES:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if stage in RNASEQ_STAGES and stage != "rnaseq_candidate_family" and not has_rnaseq_marker:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if stage == "expression" and has_rnaseq_marker:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for kw in keywords:
|
|
|
|
|
|
if kw in query_lower:
|
|
|
|
|
|
stages.append(stage)
|
|
|
|
|
|
matched_keywords.append((stage, kw, len(kw)))
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# 优先级处理:如果同时匹配了 interspecies_synteny 和 synteny,只保留 interspecies_synteny
|
|
|
|
|
|
# 因为 "种间共线性" 比 "共线性" 更具体
|
|
|
|
|
|
if "interspecies_synteny" in stages and "synteny" in stages:
|
|
|
|
|
|
stages.remove("synteny")
|
|
|
|
|
|
|
|
|
|
|
|
# "启动子结合位点/TF结合位点/FIMO" 属于 TFBS 扫描,不是 PlantCARE 类启动子顺式元件分析。
|
|
|
|
|
|
if "tfbs" in stages and "promoter" in stages:
|
|
|
|
|
|
stages = [stage for stage in stages if stage != "promoter"]
|
|
|
|
|
|
|
|
|
|
|
|
if "rnaseq_go_enrich" in stages or "rnaseq_kegg_enrich" in stages:
|
|
|
|
|
|
stages = [stage for stage in stages if stage != "rnaseq_enrich"]
|
|
|
|
|
|
|
|
|
|
|
|
if any(stage in stages for stage in RNASEQ_STAGES):
|
|
|
|
|
|
stages = [stage for stage in stages if stage not in {"expression", "msa"}]
|
|
|
|
|
|
|
|
|
|
|
|
if "rnaseq_candidate_family" in stages:
|
|
|
|
|
|
stages = [stage for stage in stages if stage not in {"domain", "annotation", "identify"}]
|
|
|
|
|
|
|
|
|
|
|
|
# Explicit utility calls. These must be more constrained than ordinary
|
|
|
|
|
|
# pipeline routing so "根据修订后的家族ID重新提取蛋白" does not become
|
|
|
|
|
|
# genome+GFF extraction.
|
|
|
|
|
|
explicit_gff_extract_context = extraction_context and any(
|
|
|
|
|
|
kw in query_lower
|
|
|
|
|
|
for kw in ["gff", "gff3", "gtf", "gffread", "genome", "基因组"]
|
|
|
|
|
|
)
|
|
|
|
|
|
if explicit_gff_extract_context and not curated_member_context:
|
|
|
|
|
|
if "pep" in query_lower or "蛋白" in query_lower:
|
|
|
|
|
|
stages.append("prepare_extract_pep")
|
|
|
|
|
|
if "cds" in query_lower:
|
|
|
|
|
|
stages.append("prepare_extract_cds")
|
|
|
|
|
|
stages = [stage for stage in stages if stage != "pep"]
|
|
|
|
|
|
|
|
|
|
|
|
if any(kw in query_lower for kw in ["数据准备", "基因组准备", "genome_prepare", "prepare genome"]):
|
|
|
|
|
|
stages.append("genome_prepare")
|
|
|
|
|
|
if any(kw in query_lower for kw in ["整理文件", "组织文件", "整理数据", "标准化命名", "重命名文件", "config整理"]):
|
|
|
|
|
|
stages.append("prepare_organize")
|
|
|
|
|
|
if any(kw in query_lower for kw in ["修订gff", "规范化gff", "gff id", "gff整理"]):
|
|
|
|
|
|
stages.append("prepare_normalize_gff")
|
|
|
|
|
|
if any(kw in query_lower for kw in ["最长转录本", "最长cds", "primary transcript", "longest transcript"]):
|
|
|
|
|
|
stages.append("prepare_longest_transcript")
|
|
|
|
|
|
if any(kw in query_lower for kw in ["ncbi id", "ncbi修订", "ncbi简化", "ncbi header", "简化ncbi"]):
|
|
|
|
|
|
stages.append("prepare_simplify_ncbi")
|
|
|
|
|
|
|
|
|
|
|
|
if "identify_refine" in stages:
|
|
|
|
|
|
stages = [stage for stage in stages if stage not in {"identify", "prepare_extract_pep", "prepare_extract_cds"}]
|
|
|
|
|
|
if "subfamily_confirm" in stages:
|
|
|
|
|
|
stages = [stage for stage in stages if stage != "reference_classification"]
|
|
|
|
|
|
|
|
|
|
|
|
# 当前标准执行器没有泛 enrichment 阶段。无 RNA-seq 前缀的 GO/KEGG/富集请求
|
|
|
|
|
|
# 也应路由到已接入的 RNA-seq 富集子阶段,避免回落到 identify/HMM 依赖。
|
|
|
|
|
|
if not stages and any(kw in query_lower for kw in ["富集", "enrichment", "enrich", "go分析", "go富集", "kegg", "通路分析", "pathway"]):
|
|
|
|
|
|
if "kegg" in query_lower or "通路" in query_lower or "pathway" in query_lower:
|
|
|
|
|
|
stages = ["rnaseq_kegg_enrich"]
|
|
|
|
|
|
elif re.search(r"(^|[^a-z])go([^a-z]|$)", query_lower) or "go富集" in query_lower or "go分析" in query_lower:
|
|
|
|
|
|
stages = ["rnaseq_go_enrich"]
|
|
|
|
|
|
else:
|
|
|
|
|
|
stages = ["rnaseq_go_enrich", "rnaseq_kegg_enrich"]
|
|
|
|
|
|
|
|
|
|
|
|
# 对“rnaseq分析/转录组分析”这类泛化请求给出默认 RNA-seq 主流程,
|
|
|
|
|
|
# 避免空 stages 后在上层错误回落到 identify。
|
|
|
|
|
|
if not stages and has_rnaseq_marker:
|
|
|
|
|
|
if any(kw in query_lower for kw in ["分析", "执行", "运行", "开始", "流程", "pipeline"]):
|
|
|
|
|
|
stages = ["rnaseq_de", "rnaseq_go_enrich", "rnaseq_kegg_enrich", "rnaseq_fgsea", "rnaseq_wgcna", "rnaseq_candidate_family"]
|
|
|
|
|
|
|
|
|
|
|
|
# 只返回标准 catalog 中存在的阶段,避免 summary/旧别名进入执行链。
|
|
|
|
|
|
known_stages = set(STAGE_ORDER)
|
|
|
|
|
|
deduped = list(dict.fromkeys(stages))
|
|
|
|
|
|
return [stage for stage in deduped if stage in known_stages]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _determine_task_type(query: str, query_lower: str) -> str:
|
|
|
|
|
|
"""确定任务类型"""
|
|
|
|
|
|
if any(kw in query_lower for kw in ["可行性", "是否可以", "能不能"]):
|
|
|
|
|
|
return "feasibility"
|
|
|
|
|
|
elif any(kw in query_lower for kw in ["报告", "总结", "生成报告"]):
|
|
|
|
|
|
return "report"
|
|
|
|
|
|
else:
|
|
|
|
|
|
return "analysis"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 便捷函数
|
|
|
|
|
|
def parse_gene_family(query: str) -> Optional[str]:
|
|
|
|
|
|
"""仅提取基因家族"""
|
|
|
|
|
|
return _extract_gene_family(query, query.lower())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_species(query: str) -> Optional[str]:
|
|
|
|
|
|
"""仅提取物种"""
|
|
|
|
|
|
return _extract_species(query, query.lower())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_stages(query: str) -> List[str]:
|
|
|
|
|
|
"""仅提取分析阶段"""
|
|
|
|
|
|
return _extract_stages(query, query.lower())
|