fix: preserve gene-family stage intent and completed results

This commit is contained in:
wtx
2026-08-24 09:34:13 +08:00
parent 311a49110f
commit 4a5fdff6f0
10 changed files with 910 additions and 20 deletions
+65 -3
View File
@@ -13,6 +13,7 @@ from gfa_core.mas.stage_catalog import (
complete_dependencies as catalog_complete_dependencies,
)
from gfa_core.mas.utils.intent_parser import (
extract_negated_target_stages,
is_gwas_fmt_intent,
parse_gene_family,
parse_gene_family_scope_selection,
@@ -154,6 +155,18 @@ class IntentAgentV2(BaseAgent):
def __init__(self, llm_manager: LLMManager):
super().__init__(llm_manager)
@staticmethod
def _current_request_text(query: str) -> str:
"""Return the user request without Router-appended confirmed context."""
text = str(query or "")
boundaries = [
index
for marker in ("\n已确认上下文:", "\nConfirmed context:")
if (index := text.find(marker)) >= 0
]
request = text[:min(boundaries)] if boundaries else text
return request.strip() or text.strip()
@staticmethod
def _apply_structured_goal_context(
intent: Dict[str, Any],
@@ -237,6 +250,7 @@ class IntentAgentV2(BaseAgent):
使用LLM理解用户输入,提取完整意图信息
"""
query = message.payload.get("query", "")
request_query = self._current_request_text(query)
# 获取前端传递的 prefix(例如 "At.WRKY")
provided_prefix = message.payload.get("prefix", "")
@@ -264,7 +278,15 @@ class IntentAgentV2(BaseAgent):
# ===== 关键修改:使用公共模块的硬编码规则解析意图 =====
from gfa_core.mas.utils.intent_parser import parse_intent_from_query
base_intent = parse_intent_from_query(query)
request_intent = parse_intent_from_query(request_query)
context_intent = parse_intent_from_query(query)
base_intent = dict(request_intent)
for key in (
"gene_family", "species", "target_species", "reference_species",
"comparative_species", "scopeMode", "params", "prefix",
):
if context_intent.get(key):
base_intent[key] = context_intent[key]
print(f"[IntentAgentV2] Base intent from parser: {base_intent}")
# 如果硬编码规则解析成功(有明确阶段),直接使用
@@ -301,7 +323,22 @@ class IntentAgentV2(BaseAgent):
else:
# 硬编码规则未命中,使用 LLM 细化 stages
print(f"[IntentAgentV2] Hardcoded rule not hit, using LLM")
intent_result = await self._recognize_intent_with_llm(query, completed_stages, provided_prefix)
intent_result = await self._recognize_intent_with_llm(request_query, completed_stages, provided_prefix)
excluded_stages = set(extract_negated_target_stages(request_query))
if excluded_stages:
intent_result["stages"] = [
stage for stage in intent_result.get("stages", [])
if stage not in excluded_stages
]
if intent_result.get("primary_stage") in excluded_stages:
intent_result["primary_stage"] = intent_result["stages"][0] if intent_result["stages"] else None
if not intent_result["stages"]:
intent_result.update({
"description": "未检测到需要执行的正向分析阶段",
"needs_clarification": True,
"clarification_type": "analysis_stage",
"clarification_question": "请说明要执行的具体分析阶段",
})
# 关键修复:保护 Parser 已解析的 gene_family/species,不被 LLM 的"未知"覆盖
if base_intent.get("gene_family") and base_intent["gene_family"] not in ["未知", "待用户指定"]:
@@ -803,6 +840,22 @@ class IntentAgentV2(BaseAgent):
}
)
excluded_stages = set(extract_negated_target_stages(query))
if excluded_stages:
intent_data["stages"] = [
stage for stage in intent_data.get("stages", [])
if stage not in excluded_stages
]
if intent_data.get("primary_stage") in excluded_stages:
intent_data["primary_stage"] = intent_data["stages"][0] if intent_data["stages"] else None
if not intent_data["stages"]:
intent_data.update({
"description": "未检测到需要执行的正向分析阶段",
"needs_clarification": True,
"clarification_type": "analysis_stage",
"clarification_question": "请说明要执行的具体分析阶段",
})
# 根据依赖关系补充缺失的阶段(关键修复:确保依赖展开)
original_stages = intent_data["stages"]
intent_data["stages"] = self._complete_dependencies(original_stages)
@@ -1069,6 +1122,8 @@ class IntentAgentV2(BaseAgent):
stages = [stage for stage in stages if stage not in {"identify", "prepare_extract_pep", "prepare_extract_cds"}]
if any(stage.startswith("rnaseq") for stage in stages):
stages = [stage for stage in stages if stage not in {"expression", "msa"}]
excluded_stages = set(extract_negated_target_stages(query))
stages = [stage for stage in stages if stage not in excluded_stages]
return list(dict.fromkeys(stages))
def _fallback_recognition(self, query: str) -> Dict[str, Any]:
@@ -1077,13 +1132,20 @@ class IntentAgentV2(BaseAgent):
# 使用关键词匹配提取阶段
stages = self._extract_stages_from_query(query)
excluded_stages = set(extract_negated_target_stages(query))
if not stages:
query_lower = query.lower()
has_rnaseq_keyword = any(
kw in query_lower for kw in ["rnaseq", "rna-seq", "rna seq", "转录组", "fastq"]
)
if has_rnaseq_keyword:
stages = ["rnaseq_de", "rnaseq_go_enrich", "rnaseq_kegg_enrich", "rnaseq_fgsea", "rnaseq_wgcna"]
stages = [
stage for stage in (
"rnaseq_de", "rnaseq_go_enrich", "rnaseq_kegg_enrich",
"rnaseq_fgsea", "rnaseq_wgcna",
)
if stage not in excluded_stages
]
# 补充依赖(使用 stage_catalog 的主规划依赖链)
original_stages = stages.copy()
+31 -1
View File
@@ -4,7 +4,15 @@
import re
from typing import Optional, Dict, Any, List
from gfa_core.mas.stage_catalog import complete_dependencies as catalog_complete_dependencies
from gfa_core.mas.utils.intent_parser import is_gwas_fmt_intent, parse_gene_family_scope_selection, resolve_gene_family_report_intent, resolve_gwas_intent, resolve_population_intent
from gfa_core.mas.utils.intent_parser import (
extract_negated_target_stages,
is_gwas_fmt_intent,
parse_gene_family_scope_selection,
parse_intent_from_query,
resolve_gene_family_report_intent,
resolve_gwas_intent,
resolve_population_intent,
)
# 分析类型关键词映射(注意:key必须与orchestrator.py中的analysis_type匹配)
ANALYSIS_KEYWORDS = {
@@ -180,6 +188,28 @@ def fast_intent_recognition(query: str) -> Optional[Dict[str, Any]]:
}
except Exception:
pass
excluded_stages = extract_negated_target_stages(query)
if excluded_stages:
parsed = parse_intent_from_query(query)
target_stages = list(parsed.get("stages") or [])
if not target_stages:
return None
all_stages = _complete_dependencies(target_stages)
return {
"stage": parsed.get("primary_stage") or target_stages[0],
"stages": all_stages,
"gene_family": parsed.get("gene_family", ""),
"species": parsed.get("species", ""),
"prefix": parsed.get("prefix", ""),
"target_species": parsed.get("target_species", []),
"reference_species": parsed.get("reference_species", []),
"comparative_species": parsed.get("comparative_species", []),
"scopeMode": parsed.get("scopeMode", "target_only"),
"params": parsed.get("params", {}),
"confidence": parsed.get("confidence", 0.9),
"task_kind": parsed.get("task_kind", "pipeline"),
"method": "keyword_negation",
}
has_rnaseq_marker = any(marker in query_lower for marker in RNASEQ_MARKERS)
if has_rnaseq_marker and any(
keyword in query_lower
+52 -2
View File
@@ -1344,6 +1344,46 @@ def _build_msa_region_plot_message(params: Dict) -> str:
return "我理解你想对 MSA 指定区间进行局部绘图;请在确认弹窗中填写 alignment 起止列。"
def _stage_keyword_pattern(keyword: str) -> str:
"""Build a boundary-safe regex for one existing stage keyword."""
escaped = re.escape(keyword.lower())
if keyword and keyword[0].isascii() and keyword[0].isalnum():
escaped = rf"(?<![a-z0-9_]){escaped}"
if keyword and keyword[-1].isascii() and keyword[-1].isalnum():
escaped = rf"{escaped}(?![a-z0-9_])"
return escaped
def extract_negated_target_stages(query: str) -> List[str]:
"""Return explicitly excluded ordinary gene-family/RNA-seq target stages."""
query_lower = str(query or "").lower()
if any(marker in query_lower for marker in (
"亚家族", "subfamily", "reference_classification", "subfamily_confirm",
"build_subfamily_evidence", "subfamily_evidence", "subfamily_prediction",
)):
return []
if re.search(r"(?:不是|并非)\s*(?:不执行|不单独执行|不要|不用|无需|不做|跳过|排除|去掉)", query_lower):
return []
eligible = set(CORE_STAGES) | set(RNASEQ_STAGES) | {"expression", "annotation", "msa"}
chinese_prefix = r"(?:不单独执行|不执行|不要(?:执行|做|进行)?|不用(?:执行|做|进行)?|无需(?:执行|做|进行)?|不做|跳过|排除|去掉)\s*"
english_prefix = r"(?:without(?:\s+(?:running|executing|performing))?|skip|exclude|do\s+not\s+(?:run|execute|perform)?)\s+"
excluded = []
for stage, keywords in STAGE_KEYWORDS.items():
if stage not in eligible:
continue
for keyword in sorted(keywords, key=len, reverse=True):
keyword_pattern = _stage_keyword_pattern(keyword)
if (
re.search(chinese_prefix + keyword_pattern, query_lower)
or re.search(english_prefix + keyword_pattern, query_lower)
or re.search(r"(?:^|[\s,,;;。::])-\s*" + keyword_pattern, query_lower)
):
excluded.append(stage)
break
return list(dict.fromkeys(excluded))
def _handle_stage_resolution(query: str, query_lower: str, stages: List[str]) -> Dict:
"""
方案B: 处理阶段解析逻辑
@@ -1352,11 +1392,15 @@ def _handle_stage_resolution(query: str, query_lower: str, stages: List[str]) ->
- 有"分析"但不明确 → 引导澄清
- 完全无法识别 → 引导澄清
"""
excluded_stages = set(extract_negated_target_stages(query))
stages = [stage for stage in stages if stage not in excluded_stages]
# 情况1: 明确指定了具体阶段 → 直接执行。
# 如果同时出现“完整/全流程”等词,则以基础基因家族链路为主体,
# 再追加用户明确点名的 Expression/KaKs/PPI/种间共线性等可选阶段。
if stages:
resolved_stages = list(dict.fromkeys([*CORE_STAGES, *stages])) if _is_complete_workflow_query(query_lower) else stages
resolved_stages = [stage for stage in resolved_stages if stage not in excluded_stages]
return {
"stages": resolved_stages,
"need_clarify": False,
@@ -1368,7 +1412,13 @@ def _handle_stage_resolution(query: str, query_lower: str, stages: List[str]) ->
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"],
"stages": [
stage for stage in (
"rnaseq_de", "rnaseq_go_enrich", "rnaseq_kegg_enrich",
"rnaseq_fgsea", "rnaseq_wgcna", "rnaseq_candidate_family",
)
if stage not in excluded_stages
],
"need_clarify": False,
"confidence": 0.9,
"note": "generic_rnaseq_workflow"
@@ -1377,7 +1427,7 @@ def _handle_stage_resolution(query: str, query_lower: str, stages: List[str]) ->
# 情况2: "基因家族分析" = 基础基因家族链路
if _is_complete_workflow_query(query_lower):
return {
"stages": CORE_STAGES,
"stages": [stage for stage in CORE_STAGES if stage not in excluded_stages],
"need_clarify": False,
"confidence": 0.9,
"note": "完整流程"