wip: qa7920 sidecar + rag stream timeout
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+22
-16
@@ -22322,11 +22322,20 @@ compare: [[flesh, peel]]`;
|
||||
}
|
||||
};
|
||||
|
||||
const readRagEventStream = async (url, options = {}, timeoutMs = 300000) => {
|
||||
const readRagEventStream = async (url, options = {}, idleTimeoutMs = 45000) => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
let timeoutId = null;
|
||||
let idleTimedOut = false;
|
||||
const resetIdleTimer = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
idleTimedOut = true;
|
||||
controller.abort();
|
||||
}, idleTimeoutMs);
|
||||
};
|
||||
let completedPayload = null;
|
||||
try {
|
||||
resetIdleTimer();
|
||||
const response = await fetch(url, { ...options, signal: controller.signal });
|
||||
if (!response.ok) {
|
||||
const error = new Error(`RAG流式请求失败:HTTP ${response.status}`);
|
||||
@@ -22375,6 +22384,7 @@ compare: [[flesh, peel]]`;
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (!done && value && value.length) resetIdleTimer();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const frames = buffer.split(/\r?\n\r?\n/);
|
||||
buffer = frames.pop() || '';
|
||||
@@ -22389,8 +22399,8 @@ compare: [[flesh, peel]]`;
|
||||
}
|
||||
return completedPayload;
|
||||
} catch (err) {
|
||||
if (err?.name === 'AbortError') {
|
||||
const timeoutError = new Error('知识库检索超时(300秒未返回)。');
|
||||
if (err?.name === 'AbortError' && idleTimedOut) {
|
||||
const timeoutError = new Error(`知识库检索连接空闲超时(${Math.round(idleTimeoutMs / 1000)}秒未收到进度)。`);
|
||||
timeoutError.code = 'request_timeout';
|
||||
throw timeoutError;
|
||||
}
|
||||
@@ -22400,7 +22410,7 @@ compare: [[flesh, peel]]`;
|
||||
}
|
||||
};
|
||||
|
||||
const requestRagWithProgress = async (payload, timeoutMs = 300000) => {
|
||||
const requestRagWithProgress = async (payload, idleTimeoutMs = 45000) => {
|
||||
resetRagProgress();
|
||||
const options = {
|
||||
method: 'POST',
|
||||
@@ -22408,14 +22418,14 @@ compare: [[flesh, peel]]`;
|
||||
body: JSON.stringify(payload)
|
||||
};
|
||||
try {
|
||||
return await readRagEventStream('/api/agent/rag/stream', options, timeoutMs);
|
||||
return await readRagEventStream('/api/agent/rag/stream', options, idleTimeoutMs);
|
||||
} catch (error) {
|
||||
if (error?.code !== 'stream_unavailable') throw error;
|
||||
applyRagProgressEvent({
|
||||
stage: 'related_search_started',
|
||||
message: '正在知识库中查找相关内容……'
|
||||
});
|
||||
return await fetchJsonWithTimeout('/api/agent/rag', options, timeoutMs, '知识库检索');
|
||||
return await fetchJsonWithTimeout('/api/agent/rag', options, 900000, '知识库检索');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22611,15 +22621,11 @@ compare: [[flesh, peel]]`;
|
||||
ragHistory.value.push({ role: 'user', content: userInput, time: new Date().toLocaleTimeString() });
|
||||
|
||||
// 调用当前 bbav5 后端 RAG 代理,随页面端口走同源地址(例如 7880)
|
||||
const data = await fetchJsonWithTimeout('/api/agent/rag', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: userInput,
|
||||
mode: 'rag',
|
||||
top_k: 10
|
||||
})
|
||||
}, 120000, '知识库检索');
|
||||
const data = await requestRagWithProgress({
|
||||
query: userInput,
|
||||
mode: 'rag',
|
||||
top_k: 10
|
||||
});
|
||||
const answer = normalizeRagAnswerForDisplay(data.success === false
|
||||
? (data.message || '未找到相关信息')
|
||||
: (data.response || data.answer || '未找到相关信息'), userInput);
|
||||
|
||||
+22
-16
@@ -22196,11 +22196,20 @@ compare: [[flesh, peel]]`;
|
||||
}
|
||||
};
|
||||
|
||||
const readRagEventStream = async (url, options = {}, timeoutMs = 300000) => {
|
||||
const readRagEventStream = async (url, options = {}, idleTimeoutMs = 45000) => {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
let timeoutId = null;
|
||||
let idleTimedOut = false;
|
||||
const resetIdleTimer = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
idleTimedOut = true;
|
||||
controller.abort();
|
||||
}, idleTimeoutMs);
|
||||
};
|
||||
let completedPayload = null;
|
||||
try {
|
||||
resetIdleTimer();
|
||||
const response = await fetch(url, { ...options, signal: controller.signal });
|
||||
if (!response.ok) {
|
||||
const error = new Error(`RAG流式请求失败:HTTP ${response.status}`);
|
||||
@@ -22249,6 +22258,7 @@ compare: [[flesh, peel]]`;
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (!done && value && value.length) resetIdleTimer();
|
||||
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
||||
const frames = buffer.split(/\r?\n\r?\n/);
|
||||
buffer = frames.pop() || '';
|
||||
@@ -22263,8 +22273,8 @@ compare: [[flesh, peel]]`;
|
||||
}
|
||||
return completedPayload;
|
||||
} catch (err) {
|
||||
if (err?.name === 'AbortError') {
|
||||
const timeoutError = new Error('知识库检索超时(300秒未返回)。');
|
||||
if (err?.name === 'AbortError' && idleTimedOut) {
|
||||
const timeoutError = new Error(`知识库检索连接空闲超时(${Math.round(idleTimeoutMs / 1000)}秒未收到进度)。`);
|
||||
timeoutError.code = 'request_timeout';
|
||||
throw timeoutError;
|
||||
}
|
||||
@@ -22274,7 +22284,7 @@ compare: [[flesh, peel]]`;
|
||||
}
|
||||
};
|
||||
|
||||
const requestRagWithProgress = async (payload, timeoutMs = 300000) => {
|
||||
const requestRagWithProgress = async (payload, idleTimeoutMs = 45000) => {
|
||||
resetRagProgress();
|
||||
const options = {
|
||||
method: 'POST',
|
||||
@@ -22282,14 +22292,14 @@ compare: [[flesh, peel]]`;
|
||||
body: JSON.stringify(payload)
|
||||
};
|
||||
try {
|
||||
return await readRagEventStream('/api/agent/rag/stream', options, timeoutMs);
|
||||
return await readRagEventStream('/api/agent/rag/stream', options, idleTimeoutMs);
|
||||
} catch (error) {
|
||||
if (error?.code !== 'stream_unavailable') throw error;
|
||||
applyRagProgressEvent({
|
||||
stage: 'related_search_started',
|
||||
message: '正在知识库中查找相关内容……'
|
||||
});
|
||||
return await fetchJsonWithTimeout('/api/agent/rag', options, timeoutMs, '知识库检索');
|
||||
return await fetchJsonWithTimeout('/api/agent/rag', options, 900000, '知识库检索');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22485,15 +22495,11 @@ compare: [[flesh, peel]]`;
|
||||
ragHistory.value.push({ role: 'user', content: userInput, time: new Date().toLocaleTimeString() });
|
||||
|
||||
// 调用当前 bbav5 后端 RAG 代理,随页面端口走同源地址
|
||||
const data = await fetchJsonWithTimeout('/api/agent/rag', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: userInput,
|
||||
mode: 'rag',
|
||||
top_k: 10
|
||||
})
|
||||
}, 120000, '知识库检索');
|
||||
const data = await requestRagWithProgress({
|
||||
query: userInput,
|
||||
mode: 'rag',
|
||||
top_k: 10
|
||||
});
|
||||
const answer = normalizeRagAnswerForDisplay(data.success === false
|
||||
? (data.message || '未找到相关信息')
|
||||
: (data.response || data.answer || '未找到相关信息'), userInput);
|
||||
|
||||
+156
-1
@@ -13386,6 +13386,134 @@ def restart_project(request: Request, payload: AdminOpsRequest):
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/admin/validate-qa-sidecar")
|
||||
def validate_qa_sidecar(request: Request, payload: AdminOpsRequest):
|
||||
"""Launch the fixed full-graph QA sidecar validation workflow."""
|
||||
auth = _validate_admin_ops_request(request, payload, "validate-qa-sidecar")
|
||||
script = PROJECT_ROOT / "scripts" / "validate_qa_v1_1_73_sidecar.sh"
|
||||
if not script.is_file():
|
||||
return {
|
||||
"success": False,
|
||||
"status": "not_configured",
|
||||
"message": f"QA sidecar validation script is missing: {script}",
|
||||
"auth": auth,
|
||||
}
|
||||
|
||||
log_path = PROJECT_ROOT / ".runtime" / "qa_v1_1_73" / "sidecar_admin.log"
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
log_handle = open(log_path, "a", encoding="utf-8")
|
||||
subprocess.Popen(
|
||||
["bash", str(script)],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
stdout=log_handle,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"status": "validation_submitted",
|
||||
"message": "QA sidecar validation submitted on the fixed loopback port.",
|
||||
"auth": auth,
|
||||
"log": str(log_path),
|
||||
}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"status": "submit_failed",
|
||||
"message": str(exc),
|
||||
"auth": auth,
|
||||
"log": str(log_path),
|
||||
}
|
||||
|
||||
|
||||
def _qa_sidecar_process_status() -> Dict[str, Any]:
|
||||
runtime_dir = PROJECT_ROOT / ".runtime" / "qa_v1_1_73" / "formal_sidecar_7945"
|
||||
pidfile = runtime_dir / "qa_7945.pid"
|
||||
result: Dict[str, Any] = {
|
||||
"pidfile": str(pidfile),
|
||||
"pidfile_exists": pidfile.is_file(),
|
||||
"running": False,
|
||||
"identity_verified": False,
|
||||
}
|
||||
if not pidfile.is_file():
|
||||
return result
|
||||
|
||||
try:
|
||||
pid = int(pidfile.read_text(encoding="utf-8").strip())
|
||||
except (OSError, ValueError) as exc:
|
||||
result["error"] = f"invalid pidfile: {exc}"
|
||||
return result
|
||||
|
||||
result["pid"] = pid
|
||||
proc_dir = Path("/proc") / str(pid)
|
||||
cmdline_path = proc_dir / "cmdline"
|
||||
if not cmdline_path.is_file():
|
||||
result["status"] = "stale_pidfile"
|
||||
return result
|
||||
|
||||
try:
|
||||
cmdline = cmdline_path.read_bytes().replace(b"\0", b" ").decode("utf-8", errors="replace").strip()
|
||||
except OSError as exc:
|
||||
result["error"] = f"cannot inspect process: {exc}"
|
||||
return result
|
||||
|
||||
expected_markers = ("uvicorn", "scripts.qa_v1_1_73_server:app", "7945")
|
||||
result.update(
|
||||
{
|
||||
"running": True,
|
||||
"identity_verified": all(marker in cmdline for marker in expected_markers),
|
||||
"command": cmdline,
|
||||
"status": "running",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/admin/qa-sidecar-status")
|
||||
def qa_sidecar_status(request: Request, payload: AdminOpsRequest):
|
||||
"""Inspect only the fixed QA sidecar process on loopback port 7945."""
|
||||
auth = _validate_admin_ops_request(request, payload, "qa-sidecar-status")
|
||||
return {"success": True, "auth": auth, "sidecar": _qa_sidecar_process_status()}
|
||||
|
||||
|
||||
@app.post("/api/admin/stop-qa-sidecar")
|
||||
def stop_qa_sidecar(request: Request, payload: AdminOpsRequest):
|
||||
"""Stop only the identity-verified QA sidecar on loopback port 7945."""
|
||||
auth = _validate_admin_ops_request(request, payload, "stop-qa-sidecar")
|
||||
before = _qa_sidecar_process_status()
|
||||
if not before.get("running"):
|
||||
return {
|
||||
"success": True,
|
||||
"status": "already_stopped",
|
||||
"auth": auth,
|
||||
"before": before,
|
||||
}
|
||||
if not before.get("identity_verified"):
|
||||
return {
|
||||
"success": False,
|
||||
"status": "identity_mismatch",
|
||||
"message": "PID exists but is not the fixed QA sidecar; refusing to signal it.",
|
||||
"auth": auth,
|
||||
"before": before,
|
||||
}
|
||||
|
||||
os.kill(int(before["pid"]), signal.SIGTERM)
|
||||
deadline = time.monotonic() + 5.0
|
||||
after = _qa_sidecar_process_status()
|
||||
while after.get("running") and time.monotonic() < deadline:
|
||||
time.sleep(0.1)
|
||||
after = _qa_sidecar_process_status()
|
||||
stopped = not after.get("running")
|
||||
return {
|
||||
"success": stopped,
|
||||
"status": "stopped" if stopped else "stop_submitted",
|
||||
"auth": auth,
|
||||
"before": before,
|
||||
"after": after,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class ResourceConfigRequest(BaseModel):
|
||||
@@ -24991,6 +25119,8 @@ async def rag_query(request: RAGQueryRequest):
|
||||
|
||||
基于文献知识库回答问题
|
||||
"""
|
||||
import requests
|
||||
|
||||
try:
|
||||
from gfa_core.mas.dual_kb_qa import query_dual_kb_qa
|
||||
|
||||
@@ -25016,6 +25146,32 @@ async def rag_query(request: RAGQueryRequest):
|
||||
payload["evidence_rag_plan"] = evidence_rag_plan
|
||||
payload["augmented_query"] = augmented_query
|
||||
return payload
|
||||
except requests.Timeout:
|
||||
augmented_query = request.query or ""
|
||||
try:
|
||||
augmented_query, evidence_rag_plan = _augment_rag_query_with_candidate_context(request)
|
||||
_save_candidate_rag_references(
|
||||
request,
|
||||
evidence_rag_plan,
|
||||
augmented_query,
|
||||
sources=[],
|
||||
status="request_timeout",
|
||||
note="complete QA exceeded the configured compatibility timeout",
|
||||
)
|
||||
except Exception:
|
||||
evidence_rag_plan = {}
|
||||
return JSONResponse(
|
||||
status_code=504,
|
||||
content={
|
||||
"success": False,
|
||||
"error_code": "request_timeout",
|
||||
"message": "完整知识图谱问答在配置时限内没有返回。",
|
||||
"answer": "",
|
||||
"sources": [],
|
||||
"mode": "gene_family_mas_qa_timeout",
|
||||
"evidence_rag_plan": evidence_rag_plan,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
augmented_query = request.query or ""
|
||||
@@ -25048,7 +25204,6 @@ async def rag_query_stream(request: RAGQueryRequest):
|
||||
for frame in stream_dual_kb_qa(
|
||||
augmented_query,
|
||||
top_k=request.top_k or 10,
|
||||
timeout=300.0,
|
||||
):
|
||||
if frame.startswith("event: complete"):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user