Improve pipeline monitoring and AI tab resilience

This commit is contained in:
Hyu
2026-07-02 16:34:11 +08:00
parent e32e515bc9
commit 0ce382fc8b
5 changed files with 407 additions and 303 deletions
+25
View File
@@ -88,6 +88,31 @@ test.describe('frontend CRUD smoke flows', () => {
).toBeVisible();
});
test('opens pipeline AI capabilities with malformed model options', async ({
page,
}) => {
await installLangBotApiMocks(page, { authenticated: true });
await page.goto('/home/pipelines?id=pipeline-ai');
await expect(page.locator('input[name="basic.name"]')).toBeVisible();
await page.getByRole('button', { name: /^AI$/ }).click();
await expect(page.getByText('Runtime')).toBeVisible();
await expect(
page.locator('[data-slot="card-title"]').filter({
hasText: 'Built-in Agent',
}),
).toBeVisible();
await expect(
page.locator('label').filter({
hasText: 'Model',
}),
).toBeVisible();
await expect(page.getByText('A <Select.Item')).toHaveCount(0);
await expect(page.getByText('500')).toHaveCount(0);
});
test('creates, edits, and deletes a knowledge base', async ({ page }) => {
await installLangBotApiMocks(page, { authenticated: true });
+109 -1
View File
@@ -194,6 +194,102 @@ function makePipeline(
};
}
function pipelineMetadata() {
return {
configs: [
{
name: 'ai',
label: {
en_US: 'AI Capabilities',
zh_Hans: 'AI 能力',
},
stages: [
{
name: 'runner',
label: {
en_US: 'Runtime',
zh_Hans: '运行方式',
},
config: [
{
id: 'runner',
name: 'runner',
label: {
en_US: 'Runner',
zh_Hans: '运行器',
},
type: 'select',
required: true,
default: 'local-agent',
options: [
{
name: 'local-agent',
label: {
en_US: 'Built-in Agent',
zh_Hans: '内置 Agent',
},
},
],
},
],
},
{
name: 'local-agent',
label: {
en_US: 'Built-in Agent',
zh_Hans: '内置 Agent',
},
config: [
{
id: 'model',
name: 'model',
label: {
en_US: 'Model',
zh_Hans: '模型',
},
type: 'model-fallback-selector',
required: true,
default: {
primary: 'llm-valid',
fallbacks: [],
},
},
],
},
],
},
],
};
}
function providerModelList() {
return {
models: [
{
uuid: '',
name: 'Broken Empty UUID Model',
provider_uuid: 'provider-empty',
provider: {
uuid: 'provider-empty',
name: 'Broken Provider',
requester: 'mock-provider',
},
},
{
uuid: 'llm-valid',
name: 'Valid Mock Model',
provider_uuid: 'provider-valid',
provider: {
uuid: 'provider-valid',
name: 'Mock Provider',
requester: 'mock-provider',
},
abilities: ['func_call'],
},
],
};
}
function knowledgeEngine() {
return {
plugin_id: 'builtin/minimal-knowledge',
@@ -395,8 +491,20 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
});
}
if (path === '/api/v1/provider/models/llm') {
return fulfillJson(route, providerModelList());
}
if (path === '/api/v1/provider/models/embedding') {
return fulfillJson(route, { models: [] });
}
if (path === '/api/v1/provider/models/rerank') {
return fulfillJson(route, { models: [] });
}
if (path === '/api/v1/pipelines/_/metadata') {
return fulfillJson(route, { configs: [] });
return fulfillJson(route, pipelineMetadata());
}
if (path === '/api/v1/pipelines') {
@@ -0,0 +1,195 @@
import { expect, test } from '@playwright/test';
import { installLangBotApiMocks } from './fixtures/langbot-api';
const bot = {
id: 'bot-pipeline-monitoring',
name: 'Pipeline Bot',
};
const pipeline = {
id: 'pipeline-monitoring',
name: 'Pipeline Under Test',
};
function at(minute: number) {
return `2026-07-02T10:${String(minute).padStart(2, '0')}:00Z`;
}
function message(
id: string,
role: 'user' | 'assistant',
minute: number,
content: string,
sessionId = 'session-pipeline-agent',
) {
return {
id,
timestamp: at(minute),
bot_id: bot.id,
bot_name: bot.name,
pipeline_id: pipeline.id,
pipeline_name: pipeline.name,
message_content: content,
session_id: sessionId,
status: 'success',
level: 'info',
platform: role === 'user' ? 'person' : 'bot',
user_id: 'pipeline-user',
user_name: 'Pipeline User',
runner_name: 'local-agent',
variables: '{}',
role,
};
}
function llmCall(
id: string,
minute: number,
messageId: string,
input: number,
output: number,
duration: number,
) {
return {
id,
timestamp: at(minute),
model_name: 'gpt-5.5',
input_tokens: input,
output_tokens: output,
total_tokens: input + output,
duration,
cost: 0,
status: 'success',
bot_id: bot.id,
bot_name: bot.name,
pipeline_id: pipeline.id,
pipeline_name: pipeline.name,
session_id: 'session-pipeline-agent',
message_id: messageId,
};
}
function toolCall(id: string, minute: number, messageId: string, name: string) {
return {
id,
timestamp: at(minute),
tool_name: name,
tool_source: 'native',
duration: 120,
status: 'success',
bot_id: bot.id,
bot_name: bot.name,
pipeline_id: pipeline.id,
pipeline_name: pipeline.name,
session_id: 'session-pipeline-agent',
message_id: messageId,
arguments: JSON.stringify({ query: name }),
result: JSON.stringify({ ok: true }),
};
}
function monitoringData() {
const messages = [
message(
'single-user',
'user',
1,
'Pipeline single user message without reply',
'session-pipeline-single',
),
message('agent-user', 'user', 10, 'Pipeline needs a deployment plan'),
message(
'agent-assistant-1',
'assistant',
11,
'Pipeline agent step 1: inspect repository',
),
message(
'agent-assistant-2',
'assistant',
12,
'Pipeline agent step 2: run tests',
),
message(
'agent-assistant-3',
'assistant',
13,
'Pipeline final answer: deployment ready',
),
];
const llmCalls = [
llmCall('pipeline-call-1', 10, 'agent-user', 100, 40, 180),
llmCall('pipeline-call-2', 11, 'agent-user', 140, 50, 220),
];
const toolCalls = [
toolCall('pipeline-tool-1', 11, 'agent-user', 'repo_search'),
toolCall('pipeline-tool-2', 12, 'agent-user', 'run_tests'),
];
return {
overview: {
total_messages: messages.length,
llm_calls: llmCalls.length,
embedding_calls: 0,
model_calls: llmCalls.length,
success_rate: 100,
active_sessions: 2,
},
messages,
llmCalls,
toolCalls,
embeddingCalls: [],
sessions: [],
errors: [],
totalCount: {
messages: messages.length,
llmCalls: llmCalls.length,
toolCalls: toolCalls.length,
embeddingCalls: 0,
sessions: 0,
errors: 0,
},
};
}
test.describe('pipeline monitoring conversation turns', () => {
test('uses conversation turns and folded tool calls in the pipeline dashboard', async ({
page,
}) => {
await installLangBotApiMocks(page, {
authenticated: true,
monitoringData: monitoringData(),
});
await page.goto(`/home/pipelines?id=${pipeline.id}`);
await page.getByRole('tab', { name: 'Dashboard' }).click();
await expect(page.getByText('2 conversation turns')).toBeVisible();
await expect(
page.getByText('Pipeline single user message without reply'),
).toBeVisible();
await expect(
page.getByText('Pipeline needs a deployment plan'),
).toBeVisible();
await expect(
page.getByText('Pipeline agent step 1: inspect repository'),
).toBeVisible();
await expect(page.getByText('Assistant +2')).toBeVisible();
await expect(page.getByText('2 tools')).toBeVisible();
const agentTurn = page
.locator('div[role="button"]')
.filter({ hasText: 'Pipeline needs a deployment plan' });
await expect(agentTurn).toHaveCount(1);
await agentTurn.click();
await expect(page.getByText('Tool Calls (2)')).toBeVisible();
await expect(page.getByText('#1 repo_search')).toBeVisible();
await expect(page.getByText('#2 run_tests')).toBeVisible();
await expect(page.getByText('Arguments')).toHaveCount(0);
await page.getByText('#1 repo_search').click();
await expect(page.getByText('Arguments')).toBeVisible();
await expect(page.getByText('Result')).toBeVisible();
});
});