fix(ui): improve agent debug and model test feedback

This commit is contained in:
Hyu
2026-09-03 14:51:01 +08:00
parent be0214cf4d
commit 7c64756203
7 changed files with 137 additions and 50 deletions
@@ -1,10 +1,14 @@
import quart
from langbot.pkg.provider.modelmgr import errors as provider_errors
from ....authz import Permission, has_permission
from ....context import RequestContext
from ... import group
MODEL_TEST_ERROR_CODE = 'model_test_failed'
@group.group_class('models/llm', '/api/v1/provider/models/llm')
class LLMModelsRouterGroup(group.RouterGroup):
async def initialize(self) -> None:
@@ -96,7 +100,14 @@ class LLMModelsRouterGroup(group.RouterGroup):
permission=Permission.PROVIDER_SECRET_MANAGE,
)
async def _(model_uuid: str, request_context: RequestContext) -> str:
await self.ap.llm_model_service.test_llm_model(request_context, model_uuid, await quart.request.json)
try:
await self.ap.llm_model_service.test_llm_model(
request_context,
model_uuid,
await quart.request.json,
)
except (provider_errors.RequesterError, ValueError) as exc:
return self.http_status(400, MODEL_TEST_ERROR_CODE, str(exc))
return self.success()
@@ -191,9 +202,14 @@ class EmbeddingModelsRouterGroup(group.RouterGroup):
permission=Permission.PROVIDER_SECRET_MANAGE,
)
async def _(model_uuid: str, request_context: RequestContext) -> str:
await self.ap.embedding_models_service.test_embedding_model(
request_context, model_uuid, await quart.request.json
)
try:
await self.ap.embedding_models_service.test_embedding_model(
request_context,
model_uuid,
await quart.request.json,
)
except (provider_errors.RequesterError, ValueError) as exc:
return self.http_status(400, MODEL_TEST_ERROR_CODE, str(exc))
return self.success()
@@ -288,5 +304,12 @@ class RerankModelsRouterGroup(group.RouterGroup):
permission=Permission.PROVIDER_SECRET_MANAGE,
)
async def _(model_uuid: str, request_context: RequestContext) -> str:
await self.ap.rerank_models_service.test_rerank_model(request_context, model_uuid, await quart.request.json)
try:
await self.ap.rerank_models_service.test_rerank_model(
request_context,
model_uuid,
await quart.request.json,
)
except (provider_errors.RequesterError, ValueError) as exc:
return self.http_status(400, MODEL_TEST_ERROR_CODE, str(exc))
return self.success()
+41
View File
@@ -12,6 +12,7 @@ import pytest
from unittest.mock import MagicMock, AsyncMock, Mock
from types import SimpleNamespace
from langbot.pkg.provider.modelmgr.errors import RequesterError
from tests.factories import FakeApp
@@ -111,16 +112,19 @@ def fake_provider_app():
app.llm_model_service.create_llm_model = AsyncMock(return_value={'uuid': 'new-model-uuid'})
app.llm_model_service.update_llm_model = AsyncMock(return_value={})
app.llm_model_service.delete_llm_model = AsyncMock()
app.llm_model_service.test_llm_model = AsyncMock()
# Embedding model service
app.embedding_models_service = Mock()
app.embedding_models_service.get_embedding_models = AsyncMock(return_value=[])
app.embedding_models_service.create_embedding_model = AsyncMock(return_value={'uuid': 'new-embedding-uuid'})
app.embedding_models_service.test_embedding_model = AsyncMock()
# Rerank model service
app.rerank_models_service = Mock()
app.rerank_models_service.get_rerank_models = AsyncMock(return_value=[])
app.rerank_models_service.create_rerank_model = AsyncMock(return_value={'uuid': 'new-rerank-uuid'})
app.rerank_models_service.test_rerank_model = AsyncMock()
# Model manager
app.model_mgr = Mock()
@@ -290,6 +294,43 @@ class TestModelEndpoints:
assert response.status_code == 200
@pytest.mark.asyncio
@pytest.mark.parametrize(
('service_name', 'method_name', 'path'),
[
('llm_model_service', 'test_llm_model', '/api/v1/provider/models/llm/_/test'),
(
'embedding_models_service',
'test_embedding_model',
'/api/v1/provider/models/embedding/_/test',
),
('rerank_models_service', 'test_rerank_model', '/api/v1/provider/models/rerank/_/test'),
],
)
async def test_model_test_returns_requester_error_details(
self,
quart_test_client,
fake_provider_app,
service_name,
method_name,
path,
):
test_model = getattr(getattr(fake_provider_app, service_name), method_name)
test_model.side_effect = RequesterError('API key invalid')
try:
response = await quart_test_client.post(
path,
headers={'Authorization': 'Bearer test_token'},
json={'name': 'broken-model'},
)
finally:
test_model.side_effect = None
assert response.status_code == 400
data = await response.get_json()
assert data['code'] == 'model_test_failed'
assert data['msg'] == '模型请求失败: API key invalid'
@pytest.mark.usefixtures('mock_circular_import_chain')
class TestEmbeddingModelEndpoints:
@@ -312,48 +312,55 @@ export default function AgentDebugPanel({
}
>
{entry.direction === 'error' && <AlertCircle />}
<div className="mb-2 flex items-center justify-between gap-2">
<Badge variant="outline">{entry.eventType}</Badge>
<span className="text-xs text-muted-foreground">
{entry.direction === 'output'
? t('agents.debugAgentOutput')
: entry.direction === 'error'
? t('common.error')
: t('agents.debugTestInput')}
</span>
</div>
<pre className="min-w-0 whitespace-pre-wrap break-words font-sans text-sm leading-relaxed">
{entry.text}
</pre>
{entry.detail && (
<Collapsible className="mt-3">
<CollapsibleTrigger asChild>
<Button type="button" variant="ghost" size="sm">
{t('agents.debugErrorDetails')}
<ChevronDown className="size-3.5" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<pre className="mt-2 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded-md bg-muted p-2 font-mono text-xs text-muted-foreground">
{entry.detail}
</pre>
</CollapsibleContent>
</Collapsible>
)}
{(entry.errorCode?.endsWith('.config_invalid') ||
entry.errorCode === 'runner_execution_failed' ||
entry.errorCode === 'runner.timeout') &&
onOpenRunnerConfig && (
<Button
type="button"
<div className="col-start-2 min-w-0">
<div className="mb-2 flex min-w-0 flex-wrap items-center justify-between gap-2">
<Badge
variant="outline"
size="sm"
className="mt-3"
onClick={onOpenRunnerConfig}
className="max-w-full overflow-hidden text-ellipsis"
>
{t('agents.debugReviewRunnerConfig')}
</Button>
{entry.eventType}
</Badge>
<span className="shrink-0 text-xs text-muted-foreground">
{entry.direction === 'output'
? t('agents.debugAgentOutput')
: entry.direction === 'error'
? t('common.error')
: t('agents.debugTestInput')}
</span>
</div>
<pre className="min-w-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere] font-sans text-sm leading-relaxed">
{entry.text}
</pre>
{entry.detail && (
<Collapsible className="mt-3">
<CollapsibleTrigger asChild>
<Button type="button" variant="ghost" size="sm">
{t('agents.debugErrorDetails')}
<ChevronDown className="size-3.5" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<pre className="mt-2 max-h-32 overflow-auto whitespace-pre-wrap break-words [overflow-wrap:anywhere] rounded-md bg-muted p-2 font-mono text-xs text-muted-foreground">
{entry.detail}
</pre>
</CollapsibleContent>
</Collapsible>
)}
{(entry.errorCode?.endsWith('.config_invalid') ||
entry.errorCode === 'runner_execution_failed' ||
entry.errorCode === 'runner.timeout') &&
onOpenRunnerConfig && (
<Button
type="button"
variant="outline"
size="sm"
className="mt-3"
onClick={onOpenRunnerConfig}
>
{t('agents.debugReviewRunnerConfig')}
</Button>
)}
</div>
</Alert>
))}
</div>
@@ -522,8 +522,9 @@ export default function ModelsPanel({
setTestResult({ success: true, duration });
} catch (err) {
console.error('Failed to test model', err);
toast.error(t('models.testError') + ': ' + (err as CustomApiError).msg);
setTestResult(null);
const message = (err as CustomApiError).msg || t('models.testError');
toast.error(t('models.testError') + ': ' + message);
setTestResult({ success: false, message });
} finally {
setIsTesting(false);
}
@@ -437,6 +437,14 @@ export default function AddModelPopover({
)}
</Button>
</div>
{testResult?.success === false && (
<p
role="alert"
className="rounded-md border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive [overflow-wrap:anywhere]"
>
{testResult.message}
</p>
)}
</div>
</div>
) : (
@@ -412,6 +412,14 @@ export default function ModelItem({
)}
</Button>
</div>
{testResult?.success === false && (
<p
role="alert"
className="rounded-md border border-destructive/30 bg-destructive/5 p-2 text-xs text-destructive [overflow-wrap:anywhere]"
>
{testResult.message}
</p>
)}
</div>
</PopoverContent>
</Popover>
@@ -27,10 +27,9 @@ export interface ProviderModels {
rerank: RerankModel[];
}
export interface TestResult {
success: boolean;
duration: number;
}
export type TestResult =
| { success: true; duration: number }
| { success: false; message: string };
export type SelectedScannedModel = {
model: ScannedProviderModel;