mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-08 20:30:59 +00:00
fix(agent): stabilize post-merge release paths
This commit is contained in:
@@ -81,7 +81,8 @@ async function api(page, path, options = {}) {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-Workspace-Id": localStorage.getItem("langbot_active_workspace_uuid") || "",
|
||||
"X-Workspace-Id":
|
||||
localStorage.getItem("langbot_active_workspace_uuid") || "",
|
||||
},
|
||||
body:
|
||||
options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
@@ -205,9 +206,7 @@ try {
|
||||
result.visible_signals.push("bot-created", "adapter-enabled");
|
||||
|
||||
await page.getByRole("button", { name: /Next|下一步|次へ/ }).click();
|
||||
const localAgentTitle = page
|
||||
.getByText(/^(Local Agent|本地 Agent)$/)
|
||||
.first();
|
||||
const localAgentTitle = page.getByText(/^(Local Agent|本地 Agent)$/).first();
|
||||
const localAgentCard = localAgentTitle.locator(
|
||||
'xpath=ancestor::*[@data-slot="card"][1]',
|
||||
);
|
||||
@@ -316,6 +315,24 @@ try {
|
||||
} catch (error) {
|
||||
if (!["blocked", "env_issue"].includes(result.status)) result.status = "fail";
|
||||
result.reason = result.reason || error.message;
|
||||
if (browser?.page && token && botId) {
|
||||
const [routeStatus, botLogs] = await Promise.all([
|
||||
api(
|
||||
browser.page,
|
||||
`/api/v1/platform/bots/${encodeURIComponent(botId)}/event-routes/status`,
|
||||
).catch(() => ({ status: 0, json: {} })),
|
||||
api(
|
||||
browser.page,
|
||||
`/api/v1/platform/bots/${encodeURIComponent(botId)}/logs`,
|
||||
{
|
||||
method: "POST",
|
||||
body: { from_index: -1, max_count: 50 },
|
||||
},
|
||||
).catch(() => ({ status: 0, json: {} })),
|
||||
]);
|
||||
result.runtime.failure_route_status = routeStatus.json.data || null;
|
||||
result.runtime.failure_bot_logs = botLogs.json.data || null;
|
||||
}
|
||||
if (browser?.page) await safeScreenshot(browser.page, paths.screenshot);
|
||||
} finally {
|
||||
if (browser?.page && token) {
|
||||
|
||||
@@ -1003,8 +1003,12 @@
|
||||
"rag"
|
||||
],
|
||||
"automation": "scripts/e2e/langrag-kb-retrieve.mjs",
|
||||
"setup_automation": [],
|
||||
"setup_provides_env": [],
|
||||
"setup_automation": [
|
||||
"node:scripts/e2e/ensure-langrag-sentinel-kb.mjs --write-env"
|
||||
],
|
||||
"setup_provides_env": [
|
||||
"LANGBOT_LOCAL_AGENT_RAG_KB_UUID"
|
||||
],
|
||||
"evidence_required": [
|
||||
"ui",
|
||||
"screenshot",
|
||||
|
||||
@@ -25,6 +25,10 @@ automation_env:
|
||||
automation_env_any:
|
||||
- LANGBOT_LOCAL_AGENT_RAG_KB_UUID|LANGBOT_RAG_KB_UUID
|
||||
automation_expected_text: "azalea-cobalt-7421"
|
||||
setup_automation:
|
||||
- "node:scripts/e2e/ensure-langrag-sentinel-kb.mjs --write-env"
|
||||
setup_provides_env:
|
||||
- LANGBOT_LOCAL_AGENT_RAG_KB_UUID
|
||||
preconditions:
|
||||
- "LangRAG is installed and initialized in the active LangBot instance."
|
||||
- "A working embedding model is available, preferably chroma-all-MiniLM-L6-v2 for local repeatability."
|
||||
|
||||
@@ -8,13 +8,20 @@ from pathlib import Path
|
||||
|
||||
def has_initial_failure_section(report: str) -> bool:
|
||||
folded = report.casefold()
|
||||
return any(
|
||||
if any(
|
||||
marker in folded
|
||||
for marker in (
|
||||
"initial fail",
|
||||
"initially fail",
|
||||
"baseline fail",
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
baseline_markers = ("baseline test", "before any edit", "before editing")
|
||||
failure_markers = ("failing test", "failed test", "failures=", "errors=")
|
||||
return any(marker in folded for marker in baseline_markers) and any(
|
||||
marker in folded for marker in failure_markers
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,9 +26,23 @@ class ComplexAgentTaskVerifyTests(unittest.TestCase):
|
||||
def test_accepts_baseline_failure_heading(self) -> None:
|
||||
self.assertTrue(MODULE.has_initial_failure_section("## Baseline failing tests\n- test_price"))
|
||||
|
||||
def test_accepts_baseline_run_heading_with_failures_in_body(self) -> None:
|
||||
report = """\
|
||||
## Baseline test run (before any edits)
|
||||
|
||||
The suite reported FAILED (failures=2). Failing tests:
|
||||
|
||||
- test_price
|
||||
- test_inventory
|
||||
"""
|
||||
self.assertTrue(MODULE.has_initial_failure_section(report))
|
||||
|
||||
def test_rejects_report_without_failure_section(self) -> None:
|
||||
self.assertFalse(MODULE.has_initial_failure_section("## Verification\nAll tests pass."))
|
||||
|
||||
def test_rejects_passing_baseline_without_failure_evidence(self) -> None:
|
||||
self.assertFalse(MODULE.has_initial_failure_section("## Baseline test run\nAll tests pass."))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -90,6 +90,13 @@ class AgentRunOrchestrator:
|
||||
execution_context = get_query_execution_context(execution_query)
|
||||
if not isinstance(execution_context, ExecutionContext):
|
||||
raise ValueError('Agent run requires a trusted ExecutionContext')
|
||||
event_workspace_id = str(event.workspace_id or '').strip()
|
||||
if event_workspace_id and event_workspace_id != execution_context.workspace_uuid:
|
||||
raise ValueError('Agent event Workspace does not match its trusted ExecutionContext')
|
||||
if not event_workspace_id:
|
||||
event = event.model_copy(
|
||||
update={'workspace_id': execution_context.workspace_uuid}
|
||||
)
|
||||
descriptor = await self.registry.get(
|
||||
execution_context,
|
||||
runner_id,
|
||||
|
||||
@@ -31,6 +31,7 @@ from .host_models import (
|
||||
from .config_resolver import RunnerConfigResolver
|
||||
from .resource_policy import ResourcePolicyProjector
|
||||
from . import events as runner_events
|
||||
from ...pipeline.pool import get_query_execution_context
|
||||
from ...provider.tools.toolmgr import TOOL_SOURCE_REFS_QUERY_KEY
|
||||
|
||||
|
||||
@@ -81,6 +82,7 @@ class QueryEntryAdapter:
|
||||
|
||||
# Build raw ref
|
||||
raw_ref = cls._build_raw_ref(query)
|
||||
execution_context = get_query_execution_context(query)
|
||||
|
||||
return AgentEventEnvelope(
|
||||
event_id=event.event_id or str(query.query_id),
|
||||
@@ -89,7 +91,7 @@ class QueryEntryAdapter:
|
||||
source='host_adapter',
|
||||
source_event_type=event.source_event_type,
|
||||
bot_id=query.bot_uuid,
|
||||
workspace_id=getattr(query, 'workspace_uuid', None),
|
||||
workspace_id=execution_context.workspace_uuid,
|
||||
conversation_id=conversation.conversation_id,
|
||||
thread_id=conversation.thread_id,
|
||||
actor=actor,
|
||||
|
||||
@@ -1566,6 +1566,11 @@ def _execution_context_from_tenant(context: TenantContext) -> ExecutionContext:
|
||||
|
||||
|
||||
def _execution_context_from_query(query: pipeline_query.Query) -> ExecutionContext:
|
||||
if isinstance(getattr(query, '_execution_context', None), ExecutionContext):
|
||||
# Import lazily to keep the loader module independent during app boot.
|
||||
from ....pipeline.pool import get_query_execution_context
|
||||
|
||||
return get_query_execution_context(query)
|
||||
return _execution_context_from_tenant(
|
||||
ExecutionContext(
|
||||
instance_uuid=str(getattr(query, 'instance_uuid', '') or ''),
|
||||
|
||||
@@ -30,6 +30,7 @@ from langbot.pkg.agent.runner.binding_resolver import (
|
||||
AgentBindingResolver,
|
||||
AgentBindingResolutionError,
|
||||
)
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
|
||||
|
||||
class TestQueryToEventEnvelope:
|
||||
@@ -350,9 +351,17 @@ def mock_query():
|
||||
"""Create a mock query for testing."""
|
||||
query = Mock()
|
||||
query.query_id = 123
|
||||
query.query_uuid = None
|
||||
query.workspace_uuid = 'workspace-test'
|
||||
query.bot_uuid = 'bot-uuid-123'
|
||||
query.pipeline_uuid = 'pipeline-uuid-456'
|
||||
query._execution_context = ExecutionContext(
|
||||
instance_uuid='instance-test',
|
||||
workspace_uuid=query.workspace_uuid,
|
||||
placement_generation=1,
|
||||
bot_uuid=query.bot_uuid,
|
||||
pipeline_uuid=query.pipeline_uuid,
|
||||
)
|
||||
query.launcher_type = Mock(value='person')
|
||||
query.launcher_id = 'launcher-123'
|
||||
query.sender_id = 'sender-123'
|
||||
@@ -399,9 +408,17 @@ def mock_query_no_session():
|
||||
"""Create a mock Query without session."""
|
||||
query = Mock()
|
||||
query.query_id = 456
|
||||
query.query_uuid = None
|
||||
query.workspace_uuid = 'workspace-test'
|
||||
query.bot_uuid = 'bot-uuid-456'
|
||||
query.pipeline_uuid = 'pipeline-uuid-789'
|
||||
query._execution_context = ExecutionContext(
|
||||
instance_uuid='instance-test',
|
||||
workspace_uuid=query.workspace_uuid,
|
||||
placement_generation=1,
|
||||
bot_uuid=query.bot_uuid,
|
||||
pipeline_uuid=query.pipeline_uuid,
|
||||
)
|
||||
query.launcher_type = Mock(value='person')
|
||||
query.launcher_id = 'launcher-456'
|
||||
query.sender_id = 'sender-456'
|
||||
|
||||
@@ -1012,8 +1012,22 @@ class TestQueryEntrySessionQueryId:
|
||||
]
|
||||
)
|
||||
ap = FakeApplication(plugin_connector, db_engine)
|
||||
async def build_resource_context(execution_query):
|
||||
from langbot.pkg.provider.tools.loaders.mcp import (
|
||||
_execution_context_from_query,
|
||||
)
|
||||
|
||||
context = _execution_context_from_query(execution_query)
|
||||
assert context.instance_uuid == TEST_CONTEXT.instance_uuid
|
||||
assert context.workspace_uuid == TEST_CONTEXT.workspace_uuid
|
||||
assert context.placement_generation == TEST_CONTEXT.placement_generation
|
||||
assert context.bot_uuid == 'bot_001'
|
||||
return 'Pinned documentation'
|
||||
|
||||
mcp_loader = types.SimpleNamespace(
|
||||
build_resource_context_for_query=AsyncMock(return_value='Pinned documentation')
|
||||
build_resource_context_for_query=AsyncMock(
|
||||
side_effect=build_resource_context
|
||||
)
|
||||
)
|
||||
ap.tool_mgr = types.SimpleNamespace(mcp_tool_loader=mcp_loader)
|
||||
orchestrator = AgentRunOrchestrator(ap, FakeRegistry(descriptor))
|
||||
@@ -1091,6 +1105,14 @@ class TestQueryEntrySessionQueryId:
|
||||
assert 'Pinned documentation' in plugin_connector.contexts[0]['input']['contents'][0]['text']
|
||||
assert event.input.text == 'hello'
|
||||
assert event.input.contents[0].text == 'hello'
|
||||
assert (
|
||||
plugin_connector.contexts[0]['conversation']['workspace_id']
|
||||
== TEST_CONTEXT.workspace_uuid
|
||||
)
|
||||
assert (
|
||||
plugin_connector.contexts[0]['runtime']['metadata']['workspace_id']
|
||||
== TEST_CONTEXT.workspace_uuid
|
||||
)
|
||||
assert 'Pinned documentation' not in str(execution_query.user_message.content)
|
||||
mcp_loader.build_resource_context_for_query.assert_awaited_once_with(execution_query)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ def make_query(
|
||||
use_llm_model_uuid=None,
|
||||
use_funcs: list | None = None,
|
||||
):
|
||||
return SimpleNamespace(
|
||||
query = SimpleNamespace(
|
||||
query_id=1,
|
||||
bot_uuid='bot_001',
|
||||
launcher_type='person',
|
||||
@@ -85,6 +85,8 @@ def make_query(
|
||||
use_funcs=use_funcs or [],
|
||||
pipeline_uuid='pipeline_001',
|
||||
)
|
||||
query._execution_context = TEST_CONTEXT
|
||||
return query
|
||||
|
||||
|
||||
async def build_resources(app, query, descriptor):
|
||||
|
||||
@@ -5,6 +5,7 @@ export default defineConfig({
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: process.env.CI ? undefined : 1,
|
||||
reporter: process.env.CI ? [['github'], ['list']] : 'list',
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:4173',
|
||||
|
||||
@@ -788,11 +788,40 @@ export class BackendClient extends BaseHttpClient {
|
||||
name: string,
|
||||
filepath: string,
|
||||
): Promise<string> {
|
||||
return this.getAuthenticatedObjectURL(
|
||||
return this.getAuthenticatedPluginPageURL(
|
||||
`/api/v1/plugins/${author}/${name}/authenticated-assets/${filepath}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async getAuthenticatedPluginPageURL(path: string): Promise<string> {
|
||||
const response = await this.instance.get<Blob>(path, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
if (!response.data.type.startsWith('text/html')) {
|
||||
return URL.createObjectURL(response.data);
|
||||
}
|
||||
|
||||
const html = await response.data.text();
|
||||
const pageSdkPattern =
|
||||
/<script\b[^>]*\bsrc=(['"])\/api\/v1\/plugins\/_sdk\/page-sdk\.js\1[^>]*>\s*<\/script>/i;
|
||||
if (!pageSdkPattern.test(html)) {
|
||||
return URL.createObjectURL(response.data);
|
||||
}
|
||||
|
||||
const sdkResponse = await this.instance.get<string>(
|
||||
'/api/v1/plugins/_sdk/page-sdk.js',
|
||||
{ responseType: 'text' },
|
||||
);
|
||||
const inlineSdk = sdkResponse.data.replace(/<\/script/gi, '<\\/script');
|
||||
const hydratedHtml = html.replace(
|
||||
pageSdkPattern,
|
||||
`<script>${inlineSdk}</script>`,
|
||||
);
|
||||
return URL.createObjectURL(
|
||||
new Blob([hydratedHtml], { type: response.data.type }),
|
||||
);
|
||||
}
|
||||
|
||||
public getAuthenticatedPluginIconURL(
|
||||
author: string,
|
||||
name: string,
|
||||
|
||||
@@ -15,6 +15,11 @@ async function submit(page: Page) {
|
||||
await page.getByRole('button', { name: /^Submit$/ }).click();
|
||||
}
|
||||
|
||||
async function selectPlaywrightAdapter(page: Page) {
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
|
||||
}
|
||||
|
||||
async function confirmDelete(page: Page) {
|
||||
await page
|
||||
.getByRole('dialog')
|
||||
@@ -83,22 +88,22 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
});
|
||||
|
||||
await page.goto('/home/bots?id=new');
|
||||
await selectPlaywrightAdapter(page);
|
||||
await page.locator('input[name="name"]').fill('Viewer Test Bot');
|
||||
await page
|
||||
.locator('input[name="description"]')
|
||||
.fill('Proves monitoring is ordinary resource visibility.');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
|
||||
await submit(page);
|
||||
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||
|
||||
await page.goto('/home/pipelines?id=new');
|
||||
await page.locator('input[name="basic.name"]').fill('Viewer Pipeline');
|
||||
await page.goto('/home/agents?id=new');
|
||||
await page.getByRole('button', { name: /^Pipeline/ }).click();
|
||||
await page.locator('input[name="name"]').fill('Viewer Pipeline');
|
||||
await page
|
||||
.locator('input[name="basic.description"]')
|
||||
.locator('input[name="description"]')
|
||||
.fill('Viewer monitoring permission regression.');
|
||||
await submit(page);
|
||||
await expect(page).toHaveURL(/\/home\/pipelines\?id=pipeline-1$/);
|
||||
await expect(page).toHaveURL(/\/home\/agents\?id=pipeline-1$/);
|
||||
|
||||
workspace.membership.role = 'viewer';
|
||||
workspace.permissions = ['member.view', 'resource.view', 'workspace.view'];
|
||||
@@ -110,7 +115,7 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
await page.getByRole('tab', { name: 'Logs' }).click();
|
||||
await expect(page.getByText('No logs yet')).toBeVisible();
|
||||
|
||||
await page.goto('/home/pipelines?id=pipeline-1');
|
||||
await page.goto('/home/agents?id=pipeline-1');
|
||||
await expect(page.getByRole('tab', { name: 'Dashboard' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'Debug Chat' })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: /^Save$/ })).toHaveCount(0);
|
||||
@@ -128,14 +133,13 @@ test.describe('frontend CRUD smoke flows', () => {
|
||||
await installLangBotApiMocks(page, { authenticated: true });
|
||||
|
||||
await page.goto('/home/bots?id=new');
|
||||
await selectPlaywrightAdapter(page);
|
||||
|
||||
await expect(page.locator('input[name="name"]')).toBeVisible();
|
||||
await page.locator('input[name="name"]').fill('Support Bot');
|
||||
await page
|
||||
.locator('input[name="description"]')
|
||||
.fill('Answers customer support questions.');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
|
||||
await submit(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||
@@ -336,9 +340,8 @@ test.describe('bot advanced flows', () => {
|
||||
|
||||
// Create a bot first
|
||||
await page.goto('/home/bots?id=new');
|
||||
await selectPlaywrightAdapter(page);
|
||||
await page.locator('input[name="name"]').fill('Toggle Test Bot');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
|
||||
await submit(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||
@@ -365,9 +368,8 @@ test.describe('bot advanced flows', () => {
|
||||
|
||||
// Create a bot
|
||||
await page.goto('/home/bots?id=new');
|
||||
await selectPlaywrightAdapter(page);
|
||||
await page.locator('input[name="name"]').fill('Tab Test Bot');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
|
||||
await submit(page);
|
||||
|
||||
// Verify we're on the Configuration tab
|
||||
@@ -400,12 +402,17 @@ test.describe('bot advanced flows', () => {
|
||||
|
||||
// Create a bot
|
||||
await page.goto('/home/bots?id=new');
|
||||
await selectPlaywrightAdapter(page);
|
||||
await page.locator('input[name="name"]').fill('Clean Form Bot');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
|
||||
await submit(page);
|
||||
|
||||
// After creation, save button should be disabled (form is clean)
|
||||
// Reload the persisted record so post-create initialization has completed.
|
||||
await page.reload();
|
||||
await expect(page.locator('input[name="name"]')).toHaveValue(
|
||||
'Clean Form Bot',
|
||||
);
|
||||
|
||||
// After loading, save button should be disabled (form is clean)
|
||||
const saveButton = page.getByRole('button', { name: /^Save$/ });
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
@@ -424,8 +431,7 @@ test.describe('bot advanced flows', () => {
|
||||
await page.goto('/home/bots?id=new');
|
||||
|
||||
// Select adapter but leave name empty
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
|
||||
await selectPlaywrightAdapter(page);
|
||||
await submit(page);
|
||||
|
||||
// Should show validation error for name (zod validation)
|
||||
@@ -690,9 +696,8 @@ test.describe('cross-resource flows', () => {
|
||||
|
||||
// Create a bot
|
||||
await page.goto('/home/bots?id=new');
|
||||
await selectPlaywrightAdapter(page);
|
||||
await page.locator('input[name="name"]').fill('Bound Bot');
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByRole('option', { name: 'Playwright Adapter' }).click();
|
||||
await submit(page);
|
||||
await expect(page).toHaveURL(/\/home\/bots\?id=bot-1$/);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ interface PipelineMock {
|
||||
description: string;
|
||||
config: JsonRecord;
|
||||
emoji: string;
|
||||
kind: 'agent' | 'pipeline';
|
||||
is_default: boolean;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -62,6 +63,7 @@ interface BotMock {
|
||||
adapter: string;
|
||||
adapter_config: JsonRecord;
|
||||
use_pipeline_uuid?: string;
|
||||
event_bindings: unknown[];
|
||||
pipeline_routing_rules: unknown[];
|
||||
adapter_runtime_values: JsonRecord;
|
||||
updated_at: string;
|
||||
@@ -259,29 +261,52 @@ function makePipeline(
|
||||
data: JsonRecord,
|
||||
uuid = nextId(state, 'pipeline'),
|
||||
): PipelineMock {
|
||||
const kind =
|
||||
data.kind === 'agent' || uuid.startsWith('agent-') ? 'agent' : 'pipeline';
|
||||
const runnerId = 'plugin:langbot-team/LocalAgent/default';
|
||||
const runnerConfig = {
|
||||
model: {
|
||||
primary: 'llm-valid',
|
||||
fallbacks: [],
|
||||
},
|
||||
'enable-all-tools': false,
|
||||
tools: ['unavailable_plugin_tool'],
|
||||
};
|
||||
const defaultConfig =
|
||||
kind === 'agent'
|
||||
? {
|
||||
runner: { id: runnerId, 'expire-time': 0 },
|
||||
runner_config: { [runnerId]: runnerConfig },
|
||||
}
|
||||
: {
|
||||
ai: {
|
||||
runner: { id: runnerId, 'expire-time': 0 },
|
||||
runner_config: { [runnerId]: runnerConfig },
|
||||
},
|
||||
trigger: {},
|
||||
safety: {},
|
||||
output: {},
|
||||
};
|
||||
return {
|
||||
uuid,
|
||||
name: String(data.name || ''),
|
||||
description: String(data.description || ''),
|
||||
config: (data.config as JsonRecord | undefined) || {
|
||||
ai: {},
|
||||
trigger: {},
|
||||
safety: {},
|
||||
output: {},
|
||||
},
|
||||
config: (data.config as JsonRecord | undefined) || defaultConfig,
|
||||
emoji: String(data.emoji || '⚙️'),
|
||||
kind,
|
||||
is_default: false,
|
||||
updated_at: now(),
|
||||
};
|
||||
}
|
||||
|
||||
function pipelineMetadata(withRunnerToolSelector = false) {
|
||||
const runnerId = 'plugin:langbot-team/LocalAgent/default';
|
||||
return {
|
||||
configs: [
|
||||
{
|
||||
name: 'ai',
|
||||
label: {
|
||||
en_US: 'AI Capabilities',
|
||||
en_US: 'AI Feature',
|
||||
zh_Hans: 'AI 能力',
|
||||
},
|
||||
stages: [
|
||||
@@ -293,21 +318,20 @@ function pipelineMetadata(withRunnerToolSelector = false) {
|
||||
},
|
||||
config: [
|
||||
{
|
||||
id: 'runner',
|
||||
name: 'runner',
|
||||
name: 'id',
|
||||
label: {
|
||||
en_US: 'Runner',
|
||||
zh_Hans: '运行器',
|
||||
},
|
||||
type: 'select',
|
||||
required: true,
|
||||
default: 'local-agent',
|
||||
default: runnerId,
|
||||
options: [
|
||||
{
|
||||
name: 'local-agent',
|
||||
name: runnerId,
|
||||
label: {
|
||||
en_US: 'Built-in Agent',
|
||||
zh_Hans: '内置 Agent',
|
||||
en_US: 'Local Agent',
|
||||
zh_Hans: '本地 Agent',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -315,10 +339,10 @@ function pipelineMetadata(withRunnerToolSelector = false) {
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'local-agent',
|
||||
name: runnerId,
|
||||
label: {
|
||||
en_US: 'Built-in Agent',
|
||||
zh_Hans: '内置 Agent',
|
||||
en_US: 'Local Agent',
|
||||
zh_Hans: '本地 Agent',
|
||||
},
|
||||
config: [
|
||||
{
|
||||
@@ -358,6 +382,25 @@ function pipelineMetadata(withRunnerToolSelector = false) {
|
||||
};
|
||||
}
|
||||
|
||||
function agentMetadata(withRunnerToolSelector = false) {
|
||||
const metadata = pipelineMetadata(withRunnerToolSelector);
|
||||
return {
|
||||
runner_config: metadata.configs[0],
|
||||
kinds: [
|
||||
{
|
||||
name: 'agent',
|
||||
supported_event_patterns: ['*'],
|
||||
message_only: false,
|
||||
},
|
||||
{
|
||||
name: 'pipeline',
|
||||
supported_event_patterns: ['message.*'],
|
||||
message_only: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function providerModelList() {
|
||||
return {
|
||||
models: [
|
||||
@@ -460,6 +503,7 @@ function makeBot(
|
||||
use_pipeline_uuid: data.use_pipeline_uuid
|
||||
? String(data.use_pipeline_uuid)
|
||||
: undefined,
|
||||
event_bindings: (data.event_bindings as unknown[] | undefined) || [],
|
||||
pipeline_routing_rules:
|
||||
(data.pipeline_routing_rules as unknown[] | undefined) || [],
|
||||
adapter_runtime_values: {
|
||||
@@ -629,6 +673,70 @@ async function handleBackendApi(route: Route, state: LangBotApiMockState) {
|
||||
return fulfillJson(route, { models: [] });
|
||||
}
|
||||
|
||||
if (path === '/api/v1/tools') {
|
||||
return fulfillJson(route, {
|
||||
tools: [
|
||||
{
|
||||
name: 'available_plugin_tool',
|
||||
human_desc: 'Available plugin tool for frontend E2E tests.',
|
||||
source: 'plugin',
|
||||
source_id: 'qa/plugin-smoke',
|
||||
source_name: 'qa/plugin-smoke',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/api/v1/agents/_/metadata') {
|
||||
return fulfillJson(route, agentMetadata(true));
|
||||
}
|
||||
|
||||
if (path === '/api/v1/agents') {
|
||||
if (method === 'POST') {
|
||||
const agent = makePipeline(state, parseJsonBody(route));
|
||||
state.pipelines = [
|
||||
...state.pipelines.filter((item) => item.uuid !== agent.uuid),
|
||||
agent,
|
||||
];
|
||||
return fulfillJson(route, { uuid: agent.uuid, kind: agent.kind });
|
||||
}
|
||||
|
||||
return fulfillJson(route, { agents: state.pipelines });
|
||||
}
|
||||
|
||||
const agentMatch = path.match(/^\/api\/v1\/agents\/([^/]+)$/);
|
||||
if (agentMatch) {
|
||||
const agentId = decodeURIComponent(agentMatch[1]);
|
||||
|
||||
if (method === 'PUT') {
|
||||
const agent = makePipeline(state, parseJsonBody(route), agentId);
|
||||
state.pipelines = [
|
||||
...state.pipelines.filter((item) => item.uuid !== agentId),
|
||||
agent,
|
||||
];
|
||||
return fulfillJson(route, {});
|
||||
}
|
||||
|
||||
if (method === 'DELETE') {
|
||||
state.pipelines = state.pipelines.filter((item) => item.uuid !== agentId);
|
||||
return fulfillJson(route, {});
|
||||
}
|
||||
|
||||
const agent = state.pipelines.find((item) => item.uuid === agentId);
|
||||
return fulfillJson(route, {
|
||||
agent:
|
||||
agent ||
|
||||
makePipeline(
|
||||
state,
|
||||
{
|
||||
name: agentId,
|
||||
kind: agentId.startsWith('agent-') ? 'agent' : 'pipeline',
|
||||
},
|
||||
agentId,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/api/v1/pipelines/_/metadata') {
|
||||
return fulfillJson(route, pipelineMetadata(state.withRunnerToolSelector));
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ const appRoutes = [
|
||||
bodyText: 'Select a bot from the sidebar',
|
||||
},
|
||||
{
|
||||
path: '/home/pipelines',
|
||||
heading: 'Pipelines',
|
||||
bodyText: 'Select a pipeline from the sidebar',
|
||||
path: '/home/agents',
|
||||
heading: 'Processors',
|
||||
bodyText: 'Select an Agent or Pipeline from the sidebar',
|
||||
},
|
||||
{
|
||||
path: '/home/extensions',
|
||||
|
||||
@@ -162,7 +162,7 @@ test.describe('pipeline monitoring conversation turns', () => {
|
||||
monitoringData: monitoringData(),
|
||||
});
|
||||
|
||||
await page.goto(`/home/pipelines?id=${pipeline.id}`);
|
||||
await page.goto(`/home/agents?id=${pipeline.id}`);
|
||||
await page.getByRole('tab', { name: 'Dashboard' }).click();
|
||||
|
||||
await expect(page.getByText('2 conversation turns')).toBeVisible();
|
||||
|
||||
@@ -62,6 +62,15 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
|
||||
});
|
||||
|
||||
let authenticatedAssetRequests = 0;
|
||||
let pageSdkRequests = 0;
|
||||
await page.route('**/api/v1/plugins/_sdk/page-sdk.js', async (route) => {
|
||||
pageSdkRequests += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/javascript',
|
||||
body: 'window.langbot = { onReady(callback) { callback(); } };',
|
||||
});
|
||||
});
|
||||
await page.route(
|
||||
'**/api/v1/plugins/langbot-team/LangRAG/authenticated-assets/**',
|
||||
async (route) => {
|
||||
@@ -69,7 +78,14 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html',
|
||||
body: '<!doctype html><html><body><h1>LangRAG Observability</h1></body></html>',
|
||||
body: `<!doctype html><html><body><main></main>
|
||||
<script src="/api/v1/plugins/_sdk/page-sdk.js"></script>
|
||||
<script>
|
||||
langbot.onReady(() => {
|
||||
document.querySelector('main').innerHTML = '<h1>LangRAG Observability</h1>';
|
||||
});
|
||||
</script>
|
||||
</body></html>`,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -84,5 +100,6 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({
|
||||
.getByRole('heading', { name: 'LangRAG Observability' }),
|
||||
).toBeVisible();
|
||||
expect(authenticatedAssetRequests).toBeGreaterThan(0);
|
||||
expect(pageSdkRequests).toBeGreaterThan(0);
|
||||
await expect(page.getByText('Loading...')).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -58,13 +58,14 @@ test('quota-reached create actions are disabled and explain the current limit',
|
||||
})),
|
||||
}),
|
||||
);
|
||||
await page.route('**/api/v1/pipelines**', (route) =>
|
||||
await page.route('**/api/v1/agents', (route) =>
|
||||
fulfill(route, {
|
||||
pipelines: Array.from({ length: 3 }, (_, index) => ({
|
||||
agents: Array.from({ length: 3 }, (_, index) => ({
|
||||
uuid: `pipeline-${index}`,
|
||||
name: `Pipeline ${index + 1}`,
|
||||
description: '',
|
||||
emoji: '⚙️',
|
||||
kind: 'pipeline',
|
||||
updated_at: new Date().toISOString(),
|
||||
})),
|
||||
}),
|
||||
@@ -103,8 +104,8 @@ test('quota-reached create actions are disabled and explain the current limit',
|
||||
name: 'Create Bots',
|
||||
exact: true,
|
||||
});
|
||||
const pipelineCreate = page.getByRole('button', {
|
||||
name: 'Create Pipelines',
|
||||
const processorCreate = page.getByRole('button', {
|
||||
name: 'Create Processors',
|
||||
exact: true,
|
||||
});
|
||||
const knowledgeCreate = page.getByRole('button', {
|
||||
@@ -117,7 +118,7 @@ test('quota-reached create actions are disabled and explain the current limit',
|
||||
});
|
||||
|
||||
await expect(botCreate).toBeDisabled();
|
||||
await expect(pipelineCreate).toBeDisabled();
|
||||
await expect(processorCreate).toBeDisabled();
|
||||
await expect(knowledgeCreate).toBeDisabled();
|
||||
await expect(addExtension).toBeEnabled();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user