fix: prevent agent debug from blocking chat
This commit is contained in:
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user