fix: prevent agent debug from blocking chat

This commit is contained in:
2026-07-13 12:27:07 +08:00
parent 2453816364
commit 0864f9305b
4 changed files with 43 additions and 6 deletions

View File

@@ -85,7 +85,7 @@ async def debug_agent(
retrieval_log_id=rag_result.retrieval_log_id, retrieval_log_id=rag_result.retrieval_log_id,
tool_trace=rag_result.tool_trace, tool_trace=rag_result.tool_trace,
) )
result = ModelClientService.debug_model( result = await ModelClientService.debug_model_async(
model, model,
rag_result, rag_result,
{ {

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
@@ -93,6 +94,11 @@ class ModelClientService:
"knowledgeIds": rag_result.knowledge_ids, "knowledgeIds": rag_result.knowledge_ids,
} }
@staticmethod
async def debug_model_async(model: ModelConfig, rag_result: RagResult, overrides: dict[str, Any]) -> dict[str, Any]:
"""Run the blocking provider client outside the ASGI event loop."""
return await asyncio.to_thread(ModelClientService.debug_model, model, rag_result, overrides)
def _mock_answer(rag_result: RagResult) -> str: def _mock_answer(rag_result: RagResult) -> str:
if not rag_result.is_hit: if not rag_result.is_hit:

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import date from datetime import date
import asyncio import asyncio
import json import json
import time
import pytest import pytest
import httpx import httpx
@@ -22,6 +23,7 @@ from app.services.chat_service import ChatService
from app.services.secret_service import ENCRYPTED_PREFIX, MASKED_SECRET, SecretService from app.services.secret_service import ENCRYPTED_PREFIX, MASKED_SECRET, SecretService
from app.services.security_state_service import SecurityStateService from app.services.security_state_service import SecurityStateService
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
from app.services.model_service import ModelClientService
from app.core.observability import RequestObservabilityMiddleware from app.core.observability import RequestObservabilityMiddleware
@@ -138,6 +140,26 @@ def test_request_id_is_returned_without_breaking_response():
assert response.json() == {"ok": True} assert response.json() == {"ok": True}
def test_agent_debug_model_call_does_not_block_event_loop(monkeypatch):
def blocking_debug(*_args, **_kwargs):
time.sleep(0.15)
return {"ok": True}
monkeypatch.setattr(ModelClientService, "debug_model", blocking_debug)
async def verify_concurrency():
started = time.perf_counter()
task = asyncio.create_task(ModelClientService.debug_model_async(object(), object(), {}))
await asyncio.sleep(0.02)
event_loop_delay = time.perf_counter() - started
result = await task
return event_loop_delay, result
delay, result = asyncio.run(verify_concurrency())
assert delay < 0.10
assert result == {"ok": True}
def test_readiness_reports_dependency_state(monkeypatch): def test_readiness_reports_dependency_state(monkeypatch):
from app.api import health from app.api import health

View File

@@ -23,14 +23,23 @@ function resize() {
element.style.overflowY = element.scrollHeight > 108 ? "auto" : "hidden"; element.style.overflowY = element.scrollHeight > 108 ? "auto" : "hidden";
} }
function send() { async function send() {
const text = input.value.trim(); const text = input.value.trim();
if (!text) return; if (!text) return;
emit("send", text, async (success) => { // Sending has been accepted by the composer. Clear immediately instead of
if (!success) return; // keeping the submitted text visible until the entire AI stream finishes.
input.value = ""; input.value = "";
await nextTick(); await nextTick();
resize(); resize();
emit("send", text, async (success) => {
// Restore only when the parent rejected/failed the request and the user has
// not entered replacement text in the meantime.
if (!success && !input.value) {
input.value = text;
await nextTick();
resize();
textarea.value?.focus();
}
}); });
} }