mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-14 16:36:07 +00:00
Improve pipeline monitoring and AI tab resilience
This commit is contained in:
@@ -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 });
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user