mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-29 20:05:25 +08:00
fix(agent): create pipelines with selected runner defaults
This commit is contained in:
@@ -438,6 +438,7 @@ class AgentService:
|
||||
'description': agent_data.get('description') or '',
|
||||
'emoji': agent_data.get('emoji') or '⚙️',
|
||||
},
|
||||
runner_id=agent_data.get('runner_id'),
|
||||
)
|
||||
return {'uuid': pipeline_uuid, 'kind': AGENT_KIND_PIPELINE}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import typing
|
||||
|
||||
from ....core import app
|
||||
from ....agent.runner.config_resolver import RunnerConfigResolver
|
||||
from ....agent.runner.errors import RunnerNotFoundError
|
||||
from ....entity.persistence import pipeline as persistence_pipeline
|
||||
from ....pipeline.extension_preferences import (
|
||||
normalize_extension_preferences,
|
||||
@@ -44,7 +45,11 @@ class PipelineService:
|
||||
) -> dict[str, typing.Any]:
|
||||
return {item['name']: item['default'] for item in config_schema if item.get('name') and 'default' in item}
|
||||
|
||||
async def get_default_pipeline_config(self, context: TenantContext) -> dict[str, typing.Any]:
|
||||
async def get_default_pipeline_config(
|
||||
self,
|
||||
context: TenantContext,
|
||||
runner_id: str | None = None,
|
||||
) -> dict[str, typing.Any]:
|
||||
from ....utils import paths as path_utils
|
||||
|
||||
template_path = path_utils.get_resource_path('templates/default-pipeline-config.json')
|
||||
@@ -54,15 +59,20 @@ class PipelineService:
|
||||
registry = getattr(self.ap, 'runner_registry', None)
|
||||
if registry is None:
|
||||
return config
|
||||
try:
|
||||
runners = await registry.list_runners(context, bound_plugins=None)
|
||||
except Exception as exc:
|
||||
self.ap.logger.warning(f'Failed to load Runner defaults for pipeline config: {exc}')
|
||||
return config
|
||||
if not runners:
|
||||
return config
|
||||
|
||||
selected = runners[0]
|
||||
if runner_id:
|
||||
try:
|
||||
selected = await registry.get(context, runner_id)
|
||||
except RunnerNotFoundError as exc:
|
||||
raise ValueError(f'Runner {runner_id} not found') from exc
|
||||
else:
|
||||
try:
|
||||
runners = await registry.list_runners(context, bound_plugins=None)
|
||||
except Exception as exc:
|
||||
self.ap.logger.warning(f'Failed to load Runner defaults for pipeline config: {exc}')
|
||||
return config
|
||||
if not runners:
|
||||
return config
|
||||
selected = runners[0]
|
||||
ai_config = config.setdefault('ai', {})
|
||||
runner_config = ai_config.setdefault('runner', {})
|
||||
runner_config['id'] = selected.id
|
||||
@@ -166,7 +176,14 @@ class PipelineService:
|
||||
serialized = self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
||||
return serialized if include_secret else redact_secrets(serialized)
|
||||
|
||||
async def create_pipeline(self, context: TenantContext, pipeline_data: dict, default: bool = False) -> str:
|
||||
async def create_pipeline(
|
||||
self,
|
||||
context: TenantContext,
|
||||
pipeline_data: dict,
|
||||
default: bool = False,
|
||||
*,
|
||||
runner_id: str | None = None,
|
||||
) -> str:
|
||||
workspace_uuid = require_workspace_uuid(context)
|
||||
if 'extensions_preferences' in pipeline_data:
|
||||
self._validate_extension_preferences(pipeline_data['extensions_preferences'])
|
||||
@@ -193,7 +210,7 @@ class PipelineService:
|
||||
pipeline_data['is_default'] = default
|
||||
|
||||
if 'config' not in pipeline_data or pipeline_data['config'] == {}:
|
||||
pipeline_data['config'] = await self.get_default_pipeline_config(context)
|
||||
pipeline_data['config'] = await self.get_default_pipeline_config(context, runner_id)
|
||||
RunnerConfigResolver.validate_pipeline_config(pipeline_data['config'])
|
||||
|
||||
# Ensure extensions_preferences is set with enable_all_plugins and enable_all_mcp_servers=True by default
|
||||
|
||||
@@ -762,6 +762,7 @@ class TestAgentServiceCreateUpdateDelete:
|
||||
'name': 'Pipeline Agent',
|
||||
'description': 'Legacy pipeline',
|
||||
'emoji': 'P',
|
||||
'runner_id': 'plugin:test/runner/default',
|
||||
},
|
||||
)
|
||||
await service.update_agent(
|
||||
@@ -779,6 +780,7 @@ class TestAgentServiceCreateUpdateDelete:
|
||||
'description': 'Legacy pipeline',
|
||||
'emoji': 'P',
|
||||
},
|
||||
runner_id='plugin:test/runner/default',
|
||||
)
|
||||
app.pipeline_service.update_pipeline.assert_awaited_once_with(
|
||||
WORKSPACE_UUID,
|
||||
|
||||
@@ -22,7 +22,7 @@ async def test_create_pipeline_defaults_missing_description(monkeypatch):
|
||||
service = PipelineService.__new__(PipelineService)
|
||||
service.ap = app
|
||||
|
||||
async def fake_get_default_config(context):
|
||||
async def fake_get_default_config(context, runner_id=None):
|
||||
runner_id = 'plugin:langbot-team/LocalAgent/default'
|
||||
return {
|
||||
'ai': {
|
||||
|
||||
@@ -23,6 +23,9 @@ class FakeRegistry:
|
||||
async def list_runners(self, context, bound_plugins=None):
|
||||
return self.runners
|
||||
|
||||
async def get(self, context, runner_id):
|
||||
return next(runner for runner in self.runners if runner.id == runner_id)
|
||||
|
||||
|
||||
def make_runner(runner_id: str, config_schema: list[dict]):
|
||||
parts = runner_id.removeprefix('plugin:').split('/')
|
||||
@@ -67,6 +70,28 @@ async def test_default_pipeline_config_uses_first_installed_runner_schema():
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_pipeline_config_uses_requested_runner_schema():
|
||||
first = make_runner(
|
||||
'plugin:alice/custom-agent/default',
|
||||
[{'name': 'api-key', 'type': 'string', 'default': ''}],
|
||||
)
|
||||
selected = make_runner(
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
[{'name': 'temperature', 'type': 'number', 'default': 0.2}],
|
||||
)
|
||||
ap = SimpleNamespace(
|
||||
logger=FakeLogger(),
|
||||
runner_registry=FakeRegistry([first, selected]),
|
||||
)
|
||||
ap.llm_model_service = LLMModelsService(ap)
|
||||
|
||||
config = await PipelineService(ap).get_default_pipeline_config('workspace-test', selected.id)
|
||||
|
||||
assert config['ai']['runner']['id'] == selected.id
|
||||
assert config['ai']['runner_config'] == {selected.id: {'temperature': 0.2}}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_pipeline_config_stays_neutral_without_installed_runners():
|
||||
ap = SimpleNamespace(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Bot, Workflow, Puzzle } from 'lucide-react';
|
||||
import { Bot, Workflow, Puzzle, Loader2 } from 'lucide-react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { AgentKind } from '@/app/infra/entities/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -46,7 +46,6 @@ export default function AgentCreateContent({
|
||||
const [metadata, setMetadata] = useState<GetAgentMetadataResponseData>();
|
||||
const [runner, setRunner] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const createdPipeline = useRef<string | null>(null);
|
||||
const loadMetadata = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -135,58 +134,13 @@ export default function AgentCreateContent({
|
||||
runner_config: { [runner]: parameters },
|
||||
};
|
||||
try {
|
||||
if (
|
||||
kind === 'pipeline' &&
|
||||
runnerStage?.config.some((field) =>
|
||||
[
|
||||
'llm-model-selector',
|
||||
'select-llm-model',
|
||||
'model-fallback-selector',
|
||||
].includes(field.type),
|
||||
)
|
||||
) {
|
||||
const { uuid } = await httpClient.getDefaultModel();
|
||||
if (uuid) {
|
||||
for (const field of runnerStage.config) {
|
||||
if (
|
||||
['llm-model-selector', 'select-llm-model'].includes(field.type) &&
|
||||
!parameters[field.name]
|
||||
)
|
||||
parameters[field.name] = uuid;
|
||||
if (
|
||||
field.type === 'model-fallback-selector' &&
|
||||
!parameters[field.name]?.primary
|
||||
) {
|
||||
parameters[field.name] = {
|
||||
...parameters[field.name],
|
||||
primary: uuid,
|
||||
fallbacks: parameters[field.name]?.fallbacks ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kind === 'pipeline') {
|
||||
// Keep the created ID if configuration fails, so retry does not create duplicates.
|
||||
const uuid =
|
||||
createdPipeline.current ??
|
||||
(
|
||||
await httpClient.createAgent({
|
||||
kind,
|
||||
name: values.name,
|
||||
description: '',
|
||||
emoji: values.emoji,
|
||||
})
|
||||
).uuid;
|
||||
createdPipeline.current = uuid;
|
||||
const { pipeline } = await httpClient.getPipeline(uuid);
|
||||
await httpClient.updatePipeline(uuid, {
|
||||
const { uuid } = await httpClient.createAgent({
|
||||
kind,
|
||||
name: values.name,
|
||||
description: '',
|
||||
emoji: values.emoji,
|
||||
config: {
|
||||
...pipeline.config,
|
||||
ai: { ...pipeline.config.ai, ...config },
|
||||
},
|
||||
runner_id: runner,
|
||||
});
|
||||
toast.success(t('agents.createSuccess'));
|
||||
onCreated(uuid);
|
||||
@@ -248,7 +202,12 @@ export default function AgentCreateContent({
|
||||
form="agent-create-form"
|
||||
disabled={form.formState.isSubmitting || loading || !runner}
|
||||
>
|
||||
{t('common.submit')}
|
||||
{form.formState.isSubmitting && (
|
||||
<Loader2 className="mr-2 size-4 animate-spin" aria-hidden="true" />
|
||||
)}
|
||||
{form.formState.isSubmitting
|
||||
? t('common.loading')
|
||||
: t('common.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -259,6 +259,13 @@ export interface Agent {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export type CreateAgentRequest = Pick<
|
||||
Agent,
|
||||
'name' | 'description' | 'emoji' | 'kind' | 'component_ref' | 'config'
|
||||
> & {
|
||||
runner_id?: string;
|
||||
};
|
||||
|
||||
export interface ApiRespAgents {
|
||||
agents: Agent[];
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
ApiRespAgents,
|
||||
ApiRespAgent,
|
||||
Agent,
|
||||
CreateAgentRequest,
|
||||
Pipeline,
|
||||
ApiRespPlatformAdapters,
|
||||
ApiRespPlatformAdapter,
|
||||
@@ -380,7 +381,9 @@ export class BackendClient extends BaseHttpClient {
|
||||
);
|
||||
}
|
||||
|
||||
public createAgent(agent: Agent): Promise<{ uuid: string; kind: string }> {
|
||||
public createAgent(
|
||||
agent: CreateAgentRequest,
|
||||
): Promise<{ uuid: string; kind: string }> {
|
||||
return this.post('/api/v1/agents', agent);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user