mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 00:15:20 +08:00
feat(models): add workspace starred model and processor defaults
This commit is contained in:
@@ -14,6 +14,26 @@ MODEL_TEST_ERROR_CODE = 'model_test_failed'
|
||||
@group.group_class('models/llm', '/api/v1/provider/models/llm')
|
||||
class LLMModelsRouterGroup(group.RouterGroup):
|
||||
async def initialize(self) -> None:
|
||||
@self.route('/_/starred', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
async def get_starred(request_context: RequestContext) -> str:
|
||||
return self.success(data={'uuid': await self.ap.llm_model_service.get_starred_model(request_context)})
|
||||
|
||||
@self.route('/_/starred', methods=['PUT'], permission=Permission.PROVIDER_SECRET_MANAGE)
|
||||
async def set_starred(request_context: RequestContext) -> str:
|
||||
body = await quart.request.json
|
||||
model_uuid = body.get('uuid')
|
||||
if model_uuid is not None and not isinstance(model_uuid, str):
|
||||
return self.http_status(400, -1, 'uuid must be a string or null')
|
||||
try:
|
||||
await self.ap.llm_model_service.set_starred_model(request_context, model_uuid)
|
||||
except ValueError as exc:
|
||||
return self.http_status(400, -1, str(exc))
|
||||
return self.success(data={})
|
||||
|
||||
@self.route('/_/default', methods=['GET'], permission=Permission.RESOURCE_MANAGE)
|
||||
async def get_default(request_context: RequestContext) -> str:
|
||||
return self.success(data={'uuid': await self.ap.llm_model_service.get_default_model(request_context)})
|
||||
|
||||
@self.route(
|
||||
'',
|
||||
methods=['GET'],
|
||||
|
||||
@@ -449,6 +449,11 @@ class AgentService:
|
||||
config = agent_data['config'] if 'config' in agent_data else await self._get_default_agent_config(context)
|
||||
config, runner_id, _ = RunnerConfigResolver.resolve_agent_config(config)
|
||||
await self._validate_runner_for_agent(context, runner_id)
|
||||
if runner_id:
|
||||
descriptor = await self.ap.runner_registry.get(context, runner_id)
|
||||
config['runner_config'][runner_id] = await self.ap.llm_model_service.apply_default_model(
|
||||
context, descriptor.config_schema, config['runner_config'][runner_id],
|
||||
)
|
||||
patterns = agent_data.get('supported_event_patterns', AGENT_DEFAULT_EVENT_PATTERNS)
|
||||
new_uuid = str(uuid.uuid4())
|
||||
values = {
|
||||
@@ -560,6 +565,8 @@ class AgentService:
|
||||
parameters = runner_config.get(component_ref)
|
||||
if parameters is None:
|
||||
parameters = self.ap.pipeline_service._get_default_values_from_schema(descriptor.config_schema)
|
||||
if draft:
|
||||
parameters = await self.ap.llm_model_service.apply_default_model(context, descriptor.config_schema, parameters)
|
||||
if not isinstance(parameters, dict):
|
||||
raise ValueError('Processor parameters must be an object')
|
||||
for field in descriptor.config_schema:
|
||||
|
||||
@@ -196,6 +196,74 @@ class LLMModelsService:
|
||||
def __init__(self, ap: app.Application) -> None:
|
||||
self.ap = ap
|
||||
|
||||
async def get_starred_model(self, context: TenantContext) -> str | None:
|
||||
from ....entity.persistence.metadata import WorkspaceMetadata
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(WorkspaceMetadata.value).where(
|
||||
WorkspaceMetadata.workspace_uuid == require_workspace_uuid(context),
|
||||
WorkspaceMetadata.key == 'starred_llm_model',
|
||||
)
|
||||
)
|
||||
row = result.first()
|
||||
model_uuid = row[0] if row else None
|
||||
if model_uuid and await self.get_llm_model(context, model_uuid) is not None:
|
||||
return model_uuid
|
||||
return None
|
||||
|
||||
async def set_starred_model(self, context: TenantContext, model_uuid: str | None) -> None:
|
||||
from ....entity.persistence.metadata import WorkspaceMetadata
|
||||
workspace_uuid = require_workspace_uuid(context)
|
||||
if model_uuid and await self.get_llm_model(context, model_uuid) is None:
|
||||
raise ValueError('Model not found in this Workspace')
|
||||
# A single metadata row makes concurrent selections replace each other.
|
||||
dialect = self.ap.persistence_mgr.get_db_engine().dialect.name
|
||||
if dialect == 'postgresql':
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
else:
|
||||
from sqlalchemy.dialects.sqlite import insert
|
||||
statement = insert(WorkspaceMetadata).values(
|
||||
workspace_uuid=workspace_uuid, key='starred_llm_model', value=model_uuid or '',
|
||||
)
|
||||
await self.ap.persistence_mgr.execute_async(statement.on_conflict_do_update(
|
||||
index_elements=['workspace_uuid', 'key'], set_={'value': model_uuid or ''},
|
||||
))
|
||||
|
||||
async def get_default_model(self, context: TenantContext) -> str | None:
|
||||
starred = await self.get_starred_model(context)
|
||||
if starred:
|
||||
return starred
|
||||
if self.ap.instance_config.data.get('space', {}).get('disable_models_service', False):
|
||||
return None
|
||||
owner = await self.ap.user_service.get_workspace_owner(require_workspace_uuid(context))
|
||||
cloud = getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud'
|
||||
if not cloud and not (owner and owner.space_account_uuid):
|
||||
return None
|
||||
try:
|
||||
return (await self.ap.space_service.get_recommended_chat_model(context))['uuid']
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def apply_default_model(self, context: TenantContext, schema: list[dict], parameters: dict) -> dict:
|
||||
import copy
|
||||
fields = [field for field in schema if field.get('type') in (
|
||||
'llm-model-selector', 'select-llm-model', 'model-fallback-selector',
|
||||
) and field.get('name')]
|
||||
if not fields:
|
||||
return parameters
|
||||
model_uuid = await self.get_default_model(context)
|
||||
if not model_uuid:
|
||||
return parameters
|
||||
result = copy.deepcopy(parameters)
|
||||
for field in fields:
|
||||
name = field['name']
|
||||
if field['type'] == 'model-fallback-selector':
|
||||
value = result.get(name)
|
||||
value = value if isinstance(value, dict) else {}
|
||||
if not value.get('primary'):
|
||||
result[name] = {**value, 'primary': model_uuid, 'fallbacks': value.get('fallbacks', [])}
|
||||
elif not result.get(name):
|
||||
result[name] = model_uuid
|
||||
return result
|
||||
async def get_llm_models(self, context: TenantContext, include_secret: bool = False) -> list[dict]:
|
||||
"""Get all LLM models with provider info"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
|
||||
@@ -68,6 +68,9 @@ class PipelineService:
|
||||
runner_config['id'] = selected.id
|
||||
runner_config.setdefault('expire-time', 0)
|
||||
ai_config['runner_config'] = {selected.id: self._get_default_values_from_schema(selected.config_schema)}
|
||||
ai_config['runner_config'][selected.id] = await self.ap.llm_model_service.apply_default_model(
|
||||
context, selected.config_schema, ai_config['runner_config'][selected.id],
|
||||
)
|
||||
return config
|
||||
|
||||
async def get_pipeline_metadata(self, context: TenantContext) -> list[dict]:
|
||||
|
||||
@@ -424,6 +424,22 @@ class LangBotMCPServer:
|
||||
context = _authorized(Permission.RUNTIME_OPERATE)
|
||||
return _dump(await ap.agent_service.debug_agent(context, processor_uuid, payload))
|
||||
|
||||
@mcp.tool(description='Get the single starred LLM model UUID for the current Workspace.')
|
||||
async def get_starred_model() -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
return _dump({'uuid': await ap.llm_model_service.get_starred_model(context)})
|
||||
|
||||
@mcp.tool(description='Star one LLM model for this Workspace, replacing the previous choice. Pass null to clear.')
|
||||
async def set_starred_model(model_uuid: str | None = None) -> str:
|
||||
context = _authorized(Permission.PROVIDER_SECRET_MANAGE)
|
||||
await ap.llm_model_service.set_starred_model(context, model_uuid)
|
||||
return _dump({'uuid': model_uuid})
|
||||
|
||||
@mcp.tool(description='Resolve the default LLM model: starred model, then the available Space wizard recommendation.')
|
||||
async def get_default_model() -> str:
|
||||
context = _authorized(Permission.RESOURCE_MANAGE)
|
||||
return _dump({'uuid': await ap.llm_model_service.get_default_model(context)})
|
||||
|
||||
@mcp.tool(description='List all configured LLM models. Secrets are redacted.')
|
||||
async def list_llm_models() -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
|
||||
Reference in New Issue
Block a user