mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-06 09:37:13 +00:00
fix(ui): improve agent debug and model test feedback
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user