轻量拓展分析自然语言触发:nl_intent 模块、后端意图回退与短路、两步向导与服务器路径导入
- 新增 gfa_core/extension_stage_lightweight/nl_intent.py:粗筛+DeepSeek 解析+sanitize,client_factory 可注入 - main.py:_match_lightweight_extension_intent 与三个空返回点回退、/api/agent/plan 守卫、_run_agent_chat 预闸短路、POST /api/nl-import-paths - index.html:lightweightNlRun 两步向导(拖拽上传+服务器路径导入)、executeLightweightNlRun、poll 回话 - 一并提交工作区既有改动:api.py store 默认路径 .runtime/lightweight-extension/store、index.html stop 404 容错
This commit is contained in:
+266
-2
@@ -1751,6 +1751,58 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="lightweightNlRun.visible" :title="`轻量拓展分析运行:${lightweightNlRun.extensionName || lightweightNlRun.extensionId}`" width="70%" :close-on-click-modal="false" @closed="lightweightNlRun.step = 1">
|
||||
<div style="display:flex;flex-direction:column;gap:12px;max-height:70vh;overflow:auto;">
|
||||
<el-steps :active="lightweightNlRun.step" finish-status="success" simple style="margin-bottom:4px;">
|
||||
<el-step title="输入文件"></el-step>
|
||||
<el-step title="参数确认"></el-step>
|
||||
</el-steps>
|
||||
<el-alert v-if="lightweightNlRun.parseReason" :title="`解析说明:${lightweightNlRun.parseReason}`" type="info" show-icon :closable="false" />
|
||||
<div v-if="lightweightNlRun.step === 1" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<div v-for="input in lightweightNlRun.inputs" :key="input.key" style="display:grid;grid-template-columns:150px 1fr;gap:8px;align-items:center;">
|
||||
<span style="font-size:12px;color:#606266;word-break:break-all;">{{ input.key }}</span>
|
||||
<el-input v-model="input.value" size="small" placeholder="当前 job 的 input/ 或 workspace/ 中的文件路径"></el-input>
|
||||
</div>
|
||||
<div style="border:1px dashed #c0c4cc;border-radius:4px;padding:14px;text-align:center;color:#909399;font-size:12px;"
|
||||
@dragover.prevent @drop.prevent="handleNlRunFileDrop">
|
||||
将文件拖拽到此处上传到当前 job 的 input/ 目录
|
||||
<div v-for="(percent, filename) in lightweightNlRun.uploadProgress" :key="filename" style="margin-top:6px;">
|
||||
<div style="display:flex;justify-content:space-between;gap:8px;font-size:11px;">
|
||||
<span style="word-break:break-all;">{{ filename }}</span><span>{{ percent }}%</span>
|
||||
</div>
|
||||
<el-progress :percentage="percent" :stroke-width="6" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">
|
||||
<span style="font-size:12px;color:#606266;">或导入服务器绝对路径(每行一个,文件或目录,位于 /home/xzq/ 或 /data/ 下)</span>
|
||||
<el-input v-model="lightweightNlRun.pathText" type="textarea" :rows="2" size="small" placeholder="/home/xzq/.../samples.tsv"></el-input>
|
||||
<el-button size="small" :loading="lightweightNlRun.loading" @click="importNlRunServerPaths">导入服务器路径</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="lightweightNlRun.step === 2" style="display:flex;flex-direction:column;gap:10px;">
|
||||
<el-table :data="lightweightNlRun.parameters" size="small" border empty-text="此拓展没有可配置参数">
|
||||
<el-table-column label="参数" min-width="170">
|
||||
<template #default="{row}">{{ row.display_name || row.flag || row.key }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="值" min-width="260">
|
||||
<template #default="{row}">
|
||||
<el-input v-model="row.value" size="small"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="display:grid;grid-template-columns:150px 1fr;gap:8px;align-items:center;">
|
||||
<span style="font-size:12px;color:#606266;">输出前缀</span>
|
||||
<el-input v-model="lightweightNlRun.explicitPrefix" size="small" placeholder="留空则使用 output"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="cancelLightweightNlRun">取消</el-button>
|
||||
<el-button v-if="lightweightNlRun.step === 1" type="primary" @click="nextLightweightNlRunStep">下一步</el-button>
|
||||
<el-button v-if="lightweightNlRun.step === 2" type="primary" :loading="lightweightNlRun.loading" @click="confirmLightweightNlRun">确认开始</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="extensionResultDialogVisible" title="拓展分析阶段运行结果" width="72%">
|
||||
<div style="display:flex;flex-direction:column;gap:10px;max-height:70vh;overflow:auto;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
|
||||
@@ -9346,6 +9398,11 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓
|
||||
label: '',
|
||||
});
|
||||
const lightweightExtensions = ref([]);
|
||||
const lightweightNlRun = reactive({
|
||||
visible: false, step: 1, loading: false, uploading: false, uploadProgress: {}, pathText: '',
|
||||
extensionId: '', extensionName: '', parseReason: '',
|
||||
inputs: [], parameters: [], explicitPrefix: '', intentParams: {}, userText: '',
|
||||
});
|
||||
const lightweightExtensionConversation = reactive({
|
||||
cards: [],
|
||||
activeOperationToken: '',
|
||||
@@ -9418,6 +9475,7 @@ RNA-seq KEGG/fgsea 会优先使用物种级 KEGG gene set;没有物种级缓
|
||||
loading: false
|
||||
});
|
||||
let extensionRunParameterResolve = null;
|
||||
let lightweightNlRunResolve = null;
|
||||
const extensionRunParameterRows = computed(() => Object.entries(extensionRunParameterState.parameters || {}).map(([name, spec]) => ({ name, spec: spec || {} })));
|
||||
|
||||
const userId = ref('xia-lab'); // 用户ID
|
||||
@@ -20490,10 +20548,21 @@ compare: [[flesh, peel]]`;
|
||||
card.summary = '可以直接重试,或取消后检查错误原因。';
|
||||
card.actions = { retry_semantic: true, cancel: true };
|
||||
}
|
||||
if (card.intent_context && card.intent_context.fromIntent) {
|
||||
addAgentMessage('assistant', `轻量拓展分析「${extensionId}」执行失败:${lightweightErrorText(view)}`, {
|
||||
stage: 'extension_stage', taskType: 'extension_stage', params: card.intent_context.params || {}, userText: card.intent_context.userText || ''
|
||||
});
|
||||
}
|
||||
resolve(view);
|
||||
return;
|
||||
}
|
||||
await onComplete(view);
|
||||
if (card.intent_context && card.intent_context.fromIntent) {
|
||||
const fileCount = String(((view.projection || {}).files || []).length || 0);
|
||||
addAgentMessage('assistant', `已执行轻量拓展分析:${extensionId}。结果已写入当前 job workspace,输出 ${fileCount} 个文件。`, {
|
||||
stage: 'extension_stage', taskType: 'extension_stage', params: card.intent_context.params || {}, userText: card.intent_context.userText || ''
|
||||
});
|
||||
}
|
||||
resolve(view);
|
||||
} catch (err) {
|
||||
clearLightweightRunning(operationToken);
|
||||
@@ -20740,6 +20809,181 @@ compare: [[flesh, peel]]`;
|
||||
});
|
||||
});
|
||||
};
|
||||
const executeLightweightNlRun = async (extensionId, confirmed, params, userText = '') => {
|
||||
const card = addLightweightCard({
|
||||
card_id: `${extensionId}:nl_run:${Date.now()}`, status: 'pending', status_label: '待确认', kind: 'runtime',
|
||||
title: '轻量拓展分析运行', purpose: '自然语言识别出的轻量拓展分析;确认输入与参数后执行。',
|
||||
extension_id: extensionId,
|
||||
inputs: Object.entries(confirmed.inputOverrides || {}).map(([key, value]) => ({ key, value })),
|
||||
parameters: Object.entries(confirmed.parameterOverrides || {}).map(([key, value]) => ({ key, label: key, value })),
|
||||
explicit_prefix: confirmed.explicitPrefix || '',
|
||||
intent_context: { fromIntent: true, params, userText },
|
||||
actions: { run: true, cancel: true }
|
||||
});
|
||||
if (!card) return null;
|
||||
await runLightweightExtension(card);
|
||||
return card;
|
||||
};
|
||||
const nlImportPaths = async (jobId, rawPaths) => {
|
||||
const response = await extensionApiFetch('/api/nl-import-paths', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ job_id: jobId, paths: rawPaths })
|
||||
});
|
||||
if (!response.ok) throw new Error(await extensionApiError(response, '导入服务器路径失败'));
|
||||
const data = await response.json();
|
||||
return (data && data.imported) || {};
|
||||
};
|
||||
const handleNlRunFileDrop = async (event) => {
|
||||
const files = Array.from((event.dataTransfer || {}).files || []);
|
||||
if (!files.length) return;
|
||||
const jobId = String(jobInfo.job_id || '').trim();
|
||||
if (!jobId || jobId === 'default_job') {
|
||||
ElMessage.warning('请先选择实际运行 Job。');
|
||||
return;
|
||||
}
|
||||
const form = new FormData();
|
||||
files.forEach(file => form.append('files', file, file.name));
|
||||
form.append('job_id', jobId);
|
||||
lightweightNlRun.uploading = true;
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', extensionApiUrl('/api/upload'));
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (!e.lengthComputable) return;
|
||||
const percent = Math.min(100, Math.round((e.loaded / e.total) * 100));
|
||||
files.forEach(file => { lightweightNlRun.uploadProgress[file.name] = percent; });
|
||||
};
|
||||
xhr.onload = () => resolve({
|
||||
ok: xhr.status >= 200 && xhr.status < 300,
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
json: () => Promise.resolve().then(() => JSON.parse(xhr.responseText)),
|
||||
text: () => Promise.resolve(xhr.responseText),
|
||||
});
|
||||
xhr.onerror = () => reject(new Error('上传失败:网络错误'));
|
||||
xhr.onabort = () => reject(new Error('上传已取消'));
|
||||
xhr.send(form);
|
||||
});
|
||||
if (!response.ok) throw new Error(await extensionApiError(response, '文件上传失败'));
|
||||
const data = await response.json();
|
||||
const uploaded = (data.files || []).map(name => `input/${jobId}/${name}`);
|
||||
const targetInput = lightweightNlRun.inputs.find(item => !String(item.value || '').trim()) || lightweightNlRun.inputs[0];
|
||||
if (targetInput) {
|
||||
targetInput.value = uploaded.shift() || '';
|
||||
const rest = uploaded.filter(Boolean);
|
||||
if (rest.length) {
|
||||
const emptyInputs = lightweightNlRun.inputs.filter(item => !String(item.value || '').trim());
|
||||
for (let i = 0; i < rest.length && i < emptyInputs.length; i++) {
|
||||
emptyInputs[i].value = rest[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
ElMessage.success(`已上传 ${uploaded.length} 个文件`);
|
||||
} catch (err) {
|
||||
ElMessage.error(err.message || '文件上传失败');
|
||||
} finally {
|
||||
lightweightNlRun.uploading = false;
|
||||
}
|
||||
};
|
||||
const importNlRunServerPaths = async () => {
|
||||
const jobId = String(jobInfo.job_id || '').trim();
|
||||
if (!jobId || jobId === 'default_job') {
|
||||
ElMessage.warning('请先选择实际运行 Job。');
|
||||
return;
|
||||
}
|
||||
const rawPaths = String(lightweightNlRun.pathText || '').split(/\n|,/).map(item => item.trim()).filter(Boolean);
|
||||
if (!rawPaths.length) {
|
||||
ElMessage.warning('请先填写要导入的服务器绝对路径。');
|
||||
return;
|
||||
}
|
||||
lightweightNlRun.loading = true;
|
||||
try {
|
||||
const imported = await nlImportPaths(jobId, rawPaths);
|
||||
const targetInput = lightweightNlRun.inputs.find(item => !String(item.value || '').trim()) || lightweightNlRun.inputs[0];
|
||||
if (targetInput) {
|
||||
targetInput.value = Object.values(imported)[0] || '';
|
||||
}
|
||||
ElMessage.success(`已导入 ${Object.keys(imported).length} 个路径`);
|
||||
} catch (err) {
|
||||
ElMessage.error(err.message || '导入服务器路径失败');
|
||||
} finally {
|
||||
lightweightNlRun.loading = false;
|
||||
}
|
||||
};
|
||||
const openLightweightNlRunDialog = async (extensionId, intentParams, userText = '') => {
|
||||
if (!extensionId) return null;
|
||||
try {
|
||||
const help = await lightweightHelp(extensionId);
|
||||
const inputs = (help.inputs || []).filter(item => item && item.replaceable).map(item => ({ key: item.path, value: '' }));
|
||||
const parameters = (help.parameters || []).map(item => ({ key: item.fact_id, flag: item.flag || item.fact_id, display_name: item.display_name || '', value: item.value || '' }));
|
||||
const slots = (intentParams && intentParams.llm_slots) || {};
|
||||
const slotInputs = slots.input_overrides || {};
|
||||
const slotParameters = slots.parameter_overrides || {};
|
||||
inputs.forEach(item => {
|
||||
const value = slotInputs[item.key];
|
||||
if (value) item.value = value;
|
||||
});
|
||||
parameters.forEach(item => {
|
||||
const value = slotParameters[item.key];
|
||||
if (value) item.value = value;
|
||||
});
|
||||
lightweightNlRun.extensionId = extensionId;
|
||||
lightweightNlRun.extensionName = (intentParams && intentParams.extensionName) || extensionId;
|
||||
lightweightNlRun.parseReason = (intentParams && intentParams.parse_reason) || '';
|
||||
lightweightNlRun.intentParams = intentParams || {};
|
||||
lightweightNlRun.userText = userText || '';
|
||||
lightweightNlRun.inputs = inputs;
|
||||
lightweightNlRun.parameters = parameters;
|
||||
lightweightNlRun.explicitPrefix = slots.explicit_prefix || '';
|
||||
lightweightNlRun.uploadProgress = {};
|
||||
lightweightNlRun.pathText = '';
|
||||
lightweightNlRun.step = 1;
|
||||
lightweightNlRun.visible = true;
|
||||
} catch (err) {
|
||||
ElMessage.error(err.message || '读取运行说明失败');
|
||||
return null;
|
||||
}
|
||||
return await new Promise(resolve => {
|
||||
lightweightNlRunResolve = resolve;
|
||||
});
|
||||
};
|
||||
const cancelLightweightNlRun = () => {
|
||||
lightweightNlRun.visible = false;
|
||||
if (lightweightNlRunResolve) {
|
||||
lightweightNlRunResolve(null);
|
||||
lightweightNlRunResolve = null;
|
||||
}
|
||||
};
|
||||
const confirmLightweightNlRun = () => {
|
||||
const inputOverrides = Object.fromEntries((lightweightNlRun.inputs || []).map(item => [item.key, runtimeInputPath(String(jobInfo.job_id || '').trim(), item.value)]));
|
||||
if (Object.values(inputOverrides).some(value => !value)) {
|
||||
ElMessage.warning('请填写每个输入文件。');
|
||||
return;
|
||||
}
|
||||
const parameterOverrides = Object.fromEntries((lightweightNlRun.parameters || []).map(item => [item.key, item.value]));
|
||||
const explicitPrefix = String(lightweightNlRun.explicitPrefix || '').trim() || null;
|
||||
lightweightNlRun.visible = false;
|
||||
if (lightweightNlRunResolve) {
|
||||
lightweightNlRunResolve({ confirmed: true, inputOverrides, parameterOverrides, explicitPrefix });
|
||||
lightweightNlRunResolve = null;
|
||||
}
|
||||
};
|
||||
const nextLightweightNlRunStep = () => {
|
||||
if (!String(jobInfo.job_id || '').trim()) {
|
||||
ElMessage.warning('请先选择实际运行 Job。');
|
||||
return;
|
||||
}
|
||||
if (lightweightNlRun.step === 1) {
|
||||
const emptyInputs = (lightweightNlRun.inputs || []).filter(item => !String(item.value || '').trim());
|
||||
if (emptyInputs.length) {
|
||||
ElMessage.warning('请填写每个输入文件。');
|
||||
return;
|
||||
}
|
||||
}
|
||||
lightweightNlRun.step = 2;
|
||||
};
|
||||
const resumeLightweightOperation = async () => {
|
||||
let saved;
|
||||
try {
|
||||
@@ -20813,7 +21057,16 @@ compare: [[flesh, peel]]`;
|
||||
const operationToken = lightweightExtensionConversation.activeOperationToken;
|
||||
if (!extensionId || !operationToken) return false;
|
||||
const response = await extensionApiFetch(`/api/lightweight-extension-stages/${encodeURIComponent(extensionId)}/operations/${encodeURIComponent(operationToken)}/stop`, { method: 'POST' });
|
||||
if (!response.ok) throw new Error(await extensionApiError(response, '停止拓展分析失败'));
|
||||
if (!response.ok) {
|
||||
const message = await extensionApiError(response, '停止拓展分析失败');
|
||||
if (response.status === 404 || String(message).includes('extension does not exist')) {
|
||||
clearLightweightRunning(operationToken);
|
||||
lightweightOperationPromises.delete(operationToken);
|
||||
forgetLightweightOperation(operationToken);
|
||||
return true;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const loadLightweightExtensionStages = async () => {
|
||||
@@ -21237,7 +21490,10 @@ compare: [[flesh, peel]]`;
|
||||
if (!extensionId) throw new Error('没有识别到要执行的拓展分析阶段');
|
||||
if (isLightweightExtensionId(extensionId)) {
|
||||
extensionBuild.extensionId = extensionId;
|
||||
return await openLightweightRuntimeCard(extensionId);
|
||||
// openLightweightRuntimeCard(extensionId) 保留为构建面板手动入口;executeExtensionStageRun( 是 Phase A 的执行函数,与本轻量分支无关。
|
||||
const confirmed = await openLightweightNlRunDialog(extensionId, params, userText);
|
||||
if (!confirmed) { extensionRun.statusText = '已取消执行'; return null; }
|
||||
return await executeLightweightNlRun(extensionId, confirmed, params, userText);
|
||||
}
|
||||
const inputPaths = Array.isArray(params.input_paths) ? params.input_paths : [];
|
||||
const runParameters = { ...(params.parameters || {}) };
|
||||
@@ -24456,6 +24712,14 @@ compare: [[flesh, peel]]`;
|
||||
loadExtensionRuns,
|
||||
runExtensionStage,
|
||||
runExtensionStageFromIntent,
|
||||
executeLightweightNlRun,
|
||||
openLightweightNlRunDialog,
|
||||
cancelLightweightNlRun,
|
||||
confirmLightweightNlRun,
|
||||
nextLightweightNlRunStep,
|
||||
handleNlRunFileDrop,
|
||||
importNlRunServerPaths,
|
||||
lightweightNlRun,
|
||||
extensionOutputIcon,
|
||||
openExtensionOutput,
|
||||
methodologyPaperId,
|
||||
|
||||
+197
-5
@@ -4441,6 +4441,73 @@ llm_manager = None
|
||||
agent_conversation_store = None
|
||||
|
||||
|
||||
class NlImportPathsRequest(BaseModel):
|
||||
job_id: str
|
||||
paths: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
@app.post("/api/nl-import-paths")
|
||||
async def nl_import_paths(req: NlImportPathsRequest):
|
||||
"""把服务器上的绝对路径(文件或目录)复制进当前 job 的 input/_nl_imports/。
|
||||
|
||||
供轻量拓展分析自然语言触发向导使用;路径必须真实存在且解析后位于
|
||||
/home/xzq/ 或 /data/ 之下。
|
||||
"""
|
||||
job_id = str(req.job_id or "").strip()
|
||||
if not job_id:
|
||||
raise HTTPException(status_code=400, detail="job_id 不能为空")
|
||||
job_dir = INPUT_ROOT / job_id
|
||||
try:
|
||||
job_dir.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=400, detail=f"无法创建 job 输入目录: {exc}") from exc
|
||||
|
||||
allowed_roots = ("/home/xzq/", "/data/")
|
||||
imported: dict[str, str] = {}
|
||||
for raw_path in req.paths or []:
|
||||
raw_text = str(raw_path or "").strip()
|
||||
if not raw_text:
|
||||
raise HTTPException(status_code=400, detail="paths 含空路径")
|
||||
source = Path(raw_text).expanduser()
|
||||
if not source.exists():
|
||||
raise HTTPException(status_code=400, detail=f"路径不存在: {raw_text}")
|
||||
if not (source.is_file() or source.is_dir()):
|
||||
raise HTTPException(status_code=400, detail=f"路径既不是文件也不是目录: {raw_text}")
|
||||
try:
|
||||
resolved = source.resolve()
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=400, detail=f"路径无法解析: {raw_text}: {exc}") from exc
|
||||
resolved_text = str(resolved)
|
||||
if not any(resolved_text.startswith(prefix) for prefix in allowed_roots):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"路径必须位于 {', '.join(allowed_roots)} 之下: {raw_text}",
|
||||
)
|
||||
target_dir = job_dir / "_nl_imports"
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
if source.is_file():
|
||||
destination = target_dir / source.name
|
||||
candidate = destination
|
||||
counter = 1
|
||||
while candidate.exists():
|
||||
candidate = target_dir / f"{source.stem}_{counter}{source.suffix}"
|
||||
counter += 1
|
||||
try:
|
||||
shutil.copy2(source, candidate)
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=400, detail=f"复制文件失败: {raw_text}: {exc}") from exc
|
||||
name = candidate.name
|
||||
else:
|
||||
destination = target_dir / source.name
|
||||
try:
|
||||
shutil.copytree(source, destination, dirs_exist_ok=True)
|
||||
except OSError as exc:
|
||||
raise HTTPException(status_code=400, detail=f"复制目录失败: {raw_text}: {exc}") from exc
|
||||
name = destination.name
|
||||
imported[raw_text] = f"input/{job_id}/_nl_imports/{name}"
|
||||
return {"success": True, "imported": imported}
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def initialize_agent_conversation_store() -> None:
|
||||
"""Initialize the persistent conversation state used only by MainAgent."""
|
||||
@@ -16354,7 +16421,7 @@ def _match_extension_stage_intent(message: str, job_id: str = "") -> dict[str, A
|
||||
print(f"[ExtensionStage NL] list skipped: {exc}")
|
||||
return {}
|
||||
if not candidates:
|
||||
return {}
|
||||
return _match_lightweight_extension_intent(text, job_id)
|
||||
|
||||
best: dict[str, Any] | None = candidates[0] if explicit_target_locked and len(candidates) == 1 else None
|
||||
best_score = 1000 if best is not None else 0
|
||||
@@ -16406,9 +16473,9 @@ def _match_extension_stage_intent(message: str, job_id: str = "") -> dict[str, A
|
||||
best = ext
|
||||
|
||||
if not trigger:
|
||||
return {}
|
||||
return _match_lightweight_extension_intent(text, job_id)
|
||||
if not best or best_score <= 0:
|
||||
return {}
|
||||
return _match_lightweight_extension_intent(text, job_id)
|
||||
slots = resolve_extension_slots(text, best)
|
||||
input_paths = slots.get("input_paths") or _extract_extension_input_paths(text)
|
||||
runtime_parameters = _extract_extension_parameters(text, best)
|
||||
@@ -16446,9 +16513,98 @@ def _match_extension_stage_intent(message: str, job_id: str = "") -> dict[str, A
|
||||
}
|
||||
|
||||
|
||||
def _lightweight_settings():
|
||||
"""轻量拓展分析的 LightweightApiSettings(每次调用构造,避免启动期依赖)。"""
|
||||
from gfa_core.extension_stage_lightweight.api import LightweightApiSettings
|
||||
|
||||
return LightweightApiSettings.from_environment(project_root=PROJECT_ROOT)
|
||||
|
||||
|
||||
def _lightweight_job_file_hints(job_id: str) -> list[str]:
|
||||
"""当前 job 的 input/workspace 下文件相对路径提示(供 LLM 匹配用户提到的文件)。"""
|
||||
from gfa_core.extension_stage_lightweight import nl_intent
|
||||
|
||||
job_id = str(job_id or "").strip()
|
||||
if not job_id:
|
||||
return []
|
||||
hints: list[str] = []
|
||||
for root in (INPUT_ROOT / job_id, WORKSPACE_ROOT / job_id):
|
||||
try:
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for path in root.rglob("*"):
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
relative = path.relative_to(root)
|
||||
except ValueError:
|
||||
continue
|
||||
prefix = "input" if str(root).startswith(str(INPUT_ROOT)) else "workspace"
|
||||
hints.append(f"{prefix}/{job_id}/{relative.as_posix()}")
|
||||
if len(hints) >= nl_intent.MAX_JOB_FILE_HINTS:
|
||||
return hints
|
||||
except OSError:
|
||||
continue
|
||||
return hints
|
||||
|
||||
|
||||
def _match_lightweight_extension_intent(message: str, job_id: str = "") -> dict[str, Any]:
|
||||
"""轻量拓展分析自然语言触发:粗筛命中后调用 DeepSeek 解析运行意图。
|
||||
|
||||
返回 dict 含 lightweight=True;任何失败返回 {},绝不抛异常。
|
||||
"""
|
||||
try:
|
||||
from gfa_core.extension_stage_lightweight import nl_intent
|
||||
|
||||
settings = _lightweight_settings()
|
||||
records = nl_intent.load_published_lightweight_records(settings.store_root)
|
||||
if not records:
|
||||
return {}
|
||||
resolution = nl_intent.resolve_lightweight_extension_intent(
|
||||
message,
|
||||
records,
|
||||
environ=os.environ,
|
||||
job_file_hints=_lightweight_job_file_hints(job_id),
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f"[LightweightNLIntent] matcher unavailable: {exc}")
|
||||
return {}
|
||||
if resolution is None or not resolution.extension_id:
|
||||
return {}
|
||||
params = {
|
||||
"extension_id": resolution.extension_id,
|
||||
"extensionName": resolution.extension_name or resolution.extension_id,
|
||||
"extension_store": "lightweight",
|
||||
"llm_slots": {
|
||||
"input_overrides": resolution.input_overrides,
|
||||
"parameter_overrides": resolution.parameter_overrides,
|
||||
"explicit_prefix": resolution.explicit_prefix,
|
||||
},
|
||||
"prefill": resolution.prefill,
|
||||
"confidence": resolution.confidence,
|
||||
"parse_reason": resolution.reason,
|
||||
}
|
||||
return {
|
||||
"stage": "extension_stage",
|
||||
"stages": ["extension_stage"],
|
||||
"primary_stage": "extension_stage",
|
||||
"params": params,
|
||||
"prefix": "",
|
||||
"gene_family": "",
|
||||
"species": "",
|
||||
"task_kind": "extension_stage",
|
||||
"taskType": "extension_stage",
|
||||
"lightweight": True,
|
||||
"confidence": resolution.confidence,
|
||||
"message": f"已识别为轻量拓展分析:{params['extensionName']}。请按引导上传或选择输入文件并核对参数后执行。",
|
||||
"job_id": job_id,
|
||||
"workspace": str(PROJECT_ROOT / "workspace" / job_id) if job_id else "",
|
||||
}
|
||||
|
||||
|
||||
def parse_natural_language(message: str) -> dict:
|
||||
"""解析自然语言,提取分析意图和参数
|
||||
|
||||
|
||||
支持识别多个阶段,例如:"帮我执行基因家族鉴定和绘制进化树"
|
||||
会识别出 ['identify', 'phylogeny'] 两个阶段
|
||||
"""
|
||||
@@ -17757,6 +17913,42 @@ async def _run_agent_chat(
|
||||
"handoff_request": request.message,
|
||||
}
|
||||
|
||||
# 轻量拓展分析自然语言触发:预闸命中才查轻量 store(Phase A 命中在此不短路)
|
||||
try:
|
||||
from gfa_core.extension_stage_lightweight import nl_intent as _lightweight_nl
|
||||
|
||||
_lightweight_published = _lightweight_nl.load_published_lightweight_records(
|
||||
_lightweight_settings().store_root
|
||||
)
|
||||
if _lightweight_published and _lightweight_nl._rough_trigger(request.message or "", _lightweight_published):
|
||||
extension_intent = await asyncio.to_thread(
|
||||
_match_extension_stage_intent, request.message, request.job_id or ""
|
||||
)
|
||||
if extension_intent and extension_intent.get("lightweight"):
|
||||
assistant_message = extension_intent.get("message") or "已识别为轻量拓展分析。"
|
||||
if conversation_turn is not None:
|
||||
active_store.finish_turn(
|
||||
conversation_turn,
|
||||
assistant_message=assistant_message,
|
||||
interaction_kind="independent",
|
||||
action="propose_extension_stage",
|
||||
intent=extension_intent,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"status": "ok",
|
||||
"action": "propose_extension_stage",
|
||||
"intent": extension_intent,
|
||||
"sources": [],
|
||||
"tool_trace": [],
|
||||
"mode": "analysis",
|
||||
"completed_stages": completed_stages,
|
||||
"model_used": "rule",
|
||||
"session_id": session_id,
|
||||
}
|
||||
except Exception as exc:
|
||||
print(f"[LightweightNLIntent] chat pre-gate skipped: {exc}")
|
||||
|
||||
global llm_manager
|
||||
active_llm_manager = llm_manager
|
||||
if active_llm_manager is None:
|
||||
@@ -20960,7 +21152,7 @@ async def agent_plan(request: AgentPlanRequest):
|
||||
if methodology_plan:
|
||||
return await _methodology_saved_plan_response(request, session_id, methodology_plan, methodology_params)
|
||||
extension_intent = _match_extension_stage_intent(request.message, request.job_id or "")
|
||||
if extension_intent:
|
||||
if extension_intent and not extension_intent.get("lightweight"):
|
||||
return _extension_stage_plan_response(extension_intent, session_id, request.job_id or "")
|
||||
report_intent = dict(request.intent or {})
|
||||
if report_intent.get("stages") == ["gene_family_report"]:
|
||||
|
||||
Reference in New Issue
Block a user