mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-06 17:47:14 +00:00
fix(ui): improve agent debug and model test feedback
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
import quart
|
import quart
|
||||||
|
|
||||||
|
from langbot.pkg.provider.modelmgr import errors as provider_errors
|
||||||
from ....authz import Permission, has_permission
|
from ....authz import Permission, has_permission
|
||||||
from ....context import RequestContext
|
from ....context import RequestContext
|
||||||
from ... import group
|
from ... import group
|
||||||
|
|
||||||
|
|
||||||
|
MODEL_TEST_ERROR_CODE = 'model_test_failed'
|
||||||
|
|
||||||
|
|
||||||
@group.group_class('models/llm', '/api/v1/provider/models/llm')
|
@group.group_class('models/llm', '/api/v1/provider/models/llm')
|
||||||
class LLMModelsRouterGroup(group.RouterGroup):
|
class LLMModelsRouterGroup(group.RouterGroup):
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
@@ -96,7 +100,14 @@ class LLMModelsRouterGroup(group.RouterGroup):
|
|||||||
permission=Permission.PROVIDER_SECRET_MANAGE,
|
permission=Permission.PROVIDER_SECRET_MANAGE,
|
||||||
)
|
)
|
||||||
async def _(model_uuid: str, request_context: RequestContext) -> str:
|
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()
|
return self.success()
|
||||||
|
|
||||||
|
|
||||||
@@ -191,9 +202,14 @@ class EmbeddingModelsRouterGroup(group.RouterGroup):
|
|||||||
permission=Permission.PROVIDER_SECRET_MANAGE,
|
permission=Permission.PROVIDER_SECRET_MANAGE,
|
||||||
)
|
)
|
||||||
async def _(model_uuid: str, request_context: RequestContext) -> str:
|
async def _(model_uuid: str, request_context: RequestContext) -> str:
|
||||||
await self.ap.embedding_models_service.test_embedding_model(
|
try:
|
||||||
request_context, model_uuid, await quart.request.json
|
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()
|
return self.success()
|
||||||
|
|
||||||
|
|
||||||
@@ -288,5 +304,12 @@ class RerankModelsRouterGroup(group.RouterGroup):
|
|||||||
permission=Permission.PROVIDER_SECRET_MANAGE,
|
permission=Permission.PROVIDER_SECRET_MANAGE,
|
||||||
)
|
)
|
||||||
async def _(model_uuid: str, request_context: RequestContext) -> str:
|
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()
|
return self.success()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import pytest
|
|||||||
from unittest.mock import MagicMock, AsyncMock, Mock
|
from unittest.mock import MagicMock, AsyncMock, Mock
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from langbot.pkg.provider.modelmgr.errors import RequesterError
|
||||||
from tests.factories import FakeApp
|
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.create_llm_model = AsyncMock(return_value={'uuid': 'new-model-uuid'})
|
||||||
app.llm_model_service.update_llm_model = AsyncMock(return_value={})
|
app.llm_model_service.update_llm_model = AsyncMock(return_value={})
|
||||||
app.llm_model_service.delete_llm_model = AsyncMock()
|
app.llm_model_service.delete_llm_model = AsyncMock()
|
||||||
|
app.llm_model_service.test_llm_model = AsyncMock()
|
||||||
|
|
||||||
# Embedding model service
|
# Embedding model service
|
||||||
app.embedding_models_service = Mock()
|
app.embedding_models_service = Mock()
|
||||||
app.embedding_models_service.get_embedding_models = AsyncMock(return_value=[])
|
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.create_embedding_model = AsyncMock(return_value={'uuid': 'new-embedding-uuid'})
|
||||||
|
app.embedding_models_service.test_embedding_model = AsyncMock()
|
||||||
|
|
||||||
# Rerank model service
|
# Rerank model service
|
||||||
app.rerank_models_service = Mock()
|
app.rerank_models_service = Mock()
|
||||||
app.rerank_models_service.get_rerank_models = AsyncMock(return_value=[])
|
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.create_rerank_model = AsyncMock(return_value={'uuid': 'new-rerank-uuid'})
|
||||||
|
app.rerank_models_service.test_rerank_model = AsyncMock()
|
||||||
|
|
||||||
# Model manager
|
# Model manager
|
||||||
app.model_mgr = Mock()
|
app.model_mgr = Mock()
|
||||||
@@ -290,6 +294,43 @@ class TestModelEndpoints:
|
|||||||
|
|
||||||
assert response.status_code == 200
|
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')
|
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||||
class TestEmbeddingModelEndpoints:
|
class TestEmbeddingModelEndpoints:
|
||||||
|
|||||||
@@ -312,48 +312,55 @@ export default function AgentDebugPanel({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{entry.direction === 'error' && <AlertCircle />}
|
{entry.direction === 'error' && <AlertCircle />}
|
||||||
<div className="mb-2 flex items-center justify-between gap-2">
|
<div className="col-start-2 min-w-0">
|
||||||
<Badge variant="outline">{entry.eventType}</Badge>
|
<div className="mb-2 flex min-w-0 flex-wrap items-center justify-between gap-2">
|
||||||
<span className="text-xs text-muted-foreground">
|
<Badge
|
||||||
{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"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
className="max-w-full overflow-hidden text-ellipsis"
|
||||||
className="mt-3"
|
|
||||||
onClick={onOpenRunnerConfig}
|
|
||||||
>
|
>
|
||||||
{t('agents.debugReviewRunnerConfig')}
|
{entry.eventType}
|
||||||
</Button>
|
</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>
|
</Alert>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -522,8 +522,9 @@ export default function ModelsPanel({
|
|||||||
setTestResult({ success: true, duration });
|
setTestResult({ success: true, duration });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to test model', err);
|
console.error('Failed to test model', err);
|
||||||
toast.error(t('models.testError') + ': ' + (err as CustomApiError).msg);
|
const message = (err as CustomApiError).msg || t('models.testError');
|
||||||
setTestResult(null);
|
toast.error(t('models.testError') + ': ' + message);
|
||||||
|
setTestResult({ success: false, message });
|
||||||
} finally {
|
} finally {
|
||||||
setIsTesting(false);
|
setIsTesting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,6 +437,14 @@ export default function AddModelPopover({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -412,6 +412,14 @@ export default function ModelItem({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ export interface ProviderModels {
|
|||||||
rerank: RerankModel[];
|
rerank: RerankModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TestResult {
|
export type TestResult =
|
||||||
success: boolean;
|
| { success: true; duration: number }
|
||||||
duration: number;
|
| { success: false; message: string };
|
||||||
}
|
|
||||||
|
|
||||||
export type SelectedScannedModel = {
|
export type SelectedScannedModel = {
|
||||||
model: ScannedProviderModel;
|
model: ScannedProviderModel;
|
||||||
|
|||||||
Reference in New Issue
Block a user