fix: preserve gene-family stage intent and completed results
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "完整流程"
|
||||
|
||||
Reference in New Issue
Block a user