mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-24 09:25:21 +08:00
feat(api): enable CLI diagnostics on current master
This commit is contained in:
@@ -16,7 +16,7 @@ class BoxRouterGroup(group.RouterGroup):
|
||||
@self.route(
|
||||
'/status',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
@@ -42,7 +42,7 @@ class BoxRouterGroup(group.RouterGroup):
|
||||
@self.route(
|
||||
'/sessions',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.AUDIT_VIEW,
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
@@ -55,7 +55,7 @@ class BoxRouterGroup(group.RouterGroup):
|
||||
@self.route(
|
||||
'/errors',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.AUDIT_VIEW,
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
|
||||
@@ -75,7 +75,12 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
|
||||
return self.success(data=stats)
|
||||
|
||||
@self.route('/messages', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
@self.route(
|
||||
'/messages',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def get_messages(request_context: RequestContext) -> str:
|
||||
"""Get message logs"""
|
||||
# Parse query parameters
|
||||
@@ -84,8 +89,9 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
session_ids = quart.request.args.getlist('sessionId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
limit, offset = self.ap.monitoring_service.normalize_page_window(
|
||||
quart.request.args.get('limit', 100), quart.request.args.get('offset', 0)
|
||||
)
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
@@ -111,7 +117,12 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/llm-calls', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
@self.route(
|
||||
'/llm-calls',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def get_llm_calls(request_context: RequestContext) -> str:
|
||||
"""Get LLM call records"""
|
||||
# Parse query parameters
|
||||
@@ -119,8 +130,9 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
limit, offset = self.ap.monitoring_service.normalize_page_window(
|
||||
quart.request.args.get('limit', 100), quart.request.args.get('offset', 0)
|
||||
)
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
@@ -145,7 +157,12 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/tool-calls', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
@self.route(
|
||||
'/tool-calls',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def get_tool_calls(request_context: RequestContext) -> str:
|
||||
"""Get tool call records"""
|
||||
bot_ids = quart.request.args.getlist('botId')
|
||||
@@ -153,8 +170,9 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
session_ids = quart.request.args.getlist('sessionId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
limit, offset = self.ap.monitoring_service.normalize_page_window(
|
||||
quart.request.args.get('limit', 100), quart.request.args.get('offset', 0)
|
||||
)
|
||||
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
end_time = parse_iso_datetime(end_time_str)
|
||||
@@ -179,15 +197,21 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/embedding-calls', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
@self.route(
|
||||
'/embedding-calls',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def get_embedding_calls(request_context: RequestContext) -> str:
|
||||
"""Get embedding call records"""
|
||||
# Parse query parameters
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
knowledge_base_id = quart.request.args.get('knowledgeBaseId')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
limit, offset = self.ap.monitoring_service.normalize_page_window(
|
||||
quart.request.args.get('limit', 100), quart.request.args.get('offset', 0)
|
||||
)
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
@@ -211,7 +235,12 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/sessions', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
@self.route(
|
||||
'/sessions',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def get_sessions(request_context: RequestContext) -> str:
|
||||
"""Get session information"""
|
||||
# Parse query parameters
|
||||
@@ -221,8 +250,9 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
user_query = quart.request.args.get('userQuery')
|
||||
is_active_str = quart.request.args.get('isActive')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
limit, offset = self.ap.monitoring_service.normalize_page_window(
|
||||
quart.request.args.get('limit', 100), quart.request.args.get('offset', 0)
|
||||
)
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
@@ -254,7 +284,12 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/errors', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
@self.route(
|
||||
'/errors',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def get_errors(request_context: RequestContext) -> str:
|
||||
"""Get error logs"""
|
||||
# Parse query parameters
|
||||
@@ -262,8 +297,9 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
pipeline_ids = quart.request.args.getlist('pipelineId')
|
||||
start_time_str = quart.request.args.get('startTime')
|
||||
end_time_str = quart.request.args.get('endTime')
|
||||
limit = int(quart.request.args.get('limit', 100))
|
||||
offset = int(quart.request.args.get('offset', 0))
|
||||
limit, offset = self.ap.monitoring_service.normalize_page_window(
|
||||
quart.request.args.get('limit', 100), quart.request.args.get('offset', 0)
|
||||
)
|
||||
|
||||
# Parse datetime
|
||||
start_time = parse_iso_datetime(start_time_str)
|
||||
@@ -404,7 +440,12 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/sessions/<session_id>/analysis', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
@self.route(
|
||||
'/sessions/<session_id>/analysis',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def get_session_analysis(session_id: str, request_context: RequestContext) -> str:
|
||||
"""Get detailed analysis for a specific session"""
|
||||
start_time = parse_iso_datetime(quart.request.args.get('startTime'))
|
||||
@@ -421,13 +462,18 @@ class MonitoringRouterGroup(group.RouterGroup):
|
||||
# The frontend will handle the 'found: false' case
|
||||
return self.success(data=analysis)
|
||||
|
||||
@self.route('/messages/<message_id>/details', methods=['GET'], permission=Permission.RESOURCE_VIEW)
|
||||
@self.route(
|
||||
'/messages/<message_id>/details',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def get_message_details(message_id: str, request_context: RequestContext) -> str:
|
||||
"""Get detailed information for a specific message"""
|
||||
details = await self.ap.monitoring_service.get_message_details(request_context, message_id)
|
||||
|
||||
if not details.get('found'):
|
||||
return self.error(message=f'Message {message_id} not found', code=404)
|
||||
return self.http_status(404, 'resource_not_found', 'Message not found')
|
||||
|
||||
return self.success(data=details)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import quart
|
||||
from ....authz import Permission, has_permission
|
||||
from ....context import RequestContext
|
||||
from ....service.secrets import redact_secrets
|
||||
from ....service.pipeline_run import run_pipeline
|
||||
from ... import group
|
||||
from ......pipeline.extension_preferences import (
|
||||
normalize_extension_preferences,
|
||||
@@ -198,3 +199,18 @@ class PipelinesRouterGroup(group.RouterGroup):
|
||||
except ValueError as exc:
|
||||
return self.http_status(400, -1, str(exc))
|
||||
return self.success()
|
||||
|
||||
@self.route(
|
||||
'/<pipeline_uuid>/run',
|
||||
methods=['POST'],
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RUNTIME_OPERATE,
|
||||
)
|
||||
async def run(pipeline_uuid: str, request_context: RequestContext):
|
||||
body = await quart.request.get_json()
|
||||
if not isinstance(body, dict) or set(body) != {'message'}:
|
||||
return self.http_status(400, -1, 'Expected an object containing message')
|
||||
text = body['message']
|
||||
if not isinstance(text, str) or not text.strip() or len(text) > 100_000:
|
||||
return self.http_status(400, -1, 'message must contain 1..100000 characters')
|
||||
return self.success(data=await run_pipeline(self.ap, request_context, pipeline_uuid, text))
|
||||
|
||||
@@ -13,6 +13,24 @@ from .....workspace.invitation_delivery import InvitationDeliveryService
|
||||
|
||||
|
||||
SYSTEM_CAPABILITY_OPERATIONS = (
|
||||
'knowledge_engine.list',
|
||||
'knowledge_engine.creation_schema',
|
||||
'knowledge_engine.retrieval_schema',
|
||||
'knowledge_parser.list',
|
||||
'pipeline.extensions.get',
|
||||
'pipeline.extensions.update',
|
||||
'pipeline.run',
|
||||
'monitoring.messages',
|
||||
'monitoring.llm_calls',
|
||||
'monitoring.tool_calls',
|
||||
'monitoring.embedding_calls',
|
||||
'monitoring.sessions',
|
||||
'monitoring.errors',
|
||||
'monitoring.message_details',
|
||||
'monitoring.session_analysis',
|
||||
'sandbox.status',
|
||||
'sandbox.sessions',
|
||||
'sandbox.errors',
|
||||
'bot.list',
|
||||
'bot.get',
|
||||
'bot.create',
|
||||
|
||||
@@ -434,58 +434,34 @@ class KnowledgeService:
|
||||
# ================= Knowledge Engine Discovery =================
|
||||
|
||||
async def list_knowledge_engines(self, context: TenantContext) -> list[dict]:
|
||||
"""List all available Knowledge Engines from plugins."""
|
||||
"""List engines; unavailable runtimes must not look like empty catalogs."""
|
||||
require_workspace_uuid(context)
|
||||
engines = []
|
||||
|
||||
if not self.ap.plugin_connector.is_enable_plugin:
|
||||
return engines
|
||||
return []
|
||||
await self.ap.plugin_connector.require_workspace_context(context)
|
||||
|
||||
# Get KnowledgeEngine plugins
|
||||
try:
|
||||
knowledge_engines = await self.ap.plugin_connector.list_knowledge_engines()
|
||||
engines.extend(knowledge_engines)
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f'Failed to list Knowledge Engines from plugins: {e}')
|
||||
|
||||
return engines
|
||||
return await self.ap.plugin_connector.list_knowledge_engines()
|
||||
|
||||
async def list_parsers(self, context: TenantContext, mime_type: str | None = None) -> list[dict]:
|
||||
"""List available parsers, optionally filtered by MIME type."""
|
||||
"""List parsers, optionally filtered by MIME type."""
|
||||
require_workspace_uuid(context)
|
||||
if not self.ap.plugin_connector.is_enable_plugin:
|
||||
return []
|
||||
await self.ap.plugin_connector.require_workspace_context(context)
|
||||
try:
|
||||
parsers = await self.ap.plugin_connector.list_parsers()
|
||||
if mime_type:
|
||||
parsers = [p for p in parsers if mime_type in p.get('supported_mime_types', [])]
|
||||
return parsers
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f'Failed to list parsers: {e}')
|
||||
return []
|
||||
parsers = await self.ap.plugin_connector.list_parsers()
|
||||
return [p for p in parsers if mime_type in p.get('supported_mime_types', [])] if mime_type else parsers
|
||||
|
||||
async def _require_knowledge_engine(self, context: TenantContext, plugin_id: str) -> None:
|
||||
engines = await self.list_knowledge_engines(context)
|
||||
if not any(engine.get('plugin_id') == plugin_id for engine in engines):
|
||||
raise WorkspaceNotFoundError('Knowledge engine not found')
|
||||
await self.ap.plugin_connector.require_workspace_context(context)
|
||||
|
||||
async def get_engine_creation_schema(self, context: TenantContext, plugin_id: str) -> dict:
|
||||
"""Get creation settings schema for a specific Knowledge Engine."""
|
||||
require_workspace_uuid(context)
|
||||
if not self.ap.plugin_connector.is_enable_plugin:
|
||||
return {}
|
||||
await self.ap.plugin_connector.require_workspace_context(context)
|
||||
try:
|
||||
return await self.ap.plugin_connector.get_rag_creation_schema(plugin_id)
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f'Failed to get creation schema for {plugin_id}: {e}')
|
||||
return {}
|
||||
"""Get an existing engine's creation schema in this Workspace."""
|
||||
await self._require_knowledge_engine(context, plugin_id)
|
||||
return await self.ap.plugin_connector.get_rag_creation_schema(plugin_id)
|
||||
|
||||
async def get_engine_retrieval_schema(self, context: TenantContext, plugin_id: str) -> dict:
|
||||
"""Get retrieval settings schema for a specific Knowledge Engine."""
|
||||
require_workspace_uuid(context)
|
||||
if not self.ap.plugin_connector.is_enable_plugin:
|
||||
return {}
|
||||
await self.ap.plugin_connector.require_workspace_context(context)
|
||||
try:
|
||||
return await self.ap.plugin_connector.get_rag_retrieval_schema(plugin_id)
|
||||
except Exception as e:
|
||||
self.ap.logger.warning(f'Failed to get retrieval schema for {plugin_id}: {e}')
|
||||
return {}
|
||||
"""Get an existing engine's retrieval schema in this Workspace."""
|
||||
await self._require_knowledge_engine(context, plugin_id)
|
||||
return await self.ap.plugin_connector.get_rag_retrieval_schema(plugin_id)
|
||||
|
||||
@@ -332,6 +332,28 @@ class PipelineService:
|
||||
|
||||
return new_uuid
|
||||
|
||||
async def get_pipeline_extensions(self, context: TenantContext, pipeline_uuid: str) -> dict:
|
||||
pipeline = await self.get_pipeline(context, pipeline_uuid)
|
||||
if pipeline is None:
|
||||
raise WorkspaceNotFoundError('Pipeline not found')
|
||||
if self.ap.plugin_connector.is_enable_plugin:
|
||||
await self.ap.plugin_connector.require_workspace_context(context)
|
||||
plugins = await self.ap.plugin_connector.list_plugins(component_kinds=['Command', 'EventListener', 'Tool'])
|
||||
prefs = pipeline.get('extensions_preferences', {})
|
||||
return {
|
||||
'enable_all_plugins': prefs.get('enable_all_plugins', True),
|
||||
'enable_all_mcp_servers': prefs.get('enable_all_mcp_servers', True),
|
||||
'enable_all_skills': prefs.get('enable_all_skills', True),
|
||||
'bound_plugins': prefs.get('plugins', []),
|
||||
'available_plugins': redact_secrets(plugins),
|
||||
'bound_mcp_servers': prefs.get('mcp_servers', []),
|
||||
'available_mcp_servers': await self.ap.mcp_service.get_mcp_servers(context, contain_runtime_info=True),
|
||||
'bound_mcp_resources': prefs.get('mcp_resources', []),
|
||||
'mcp_resource_agent_read_enabled': prefs.get('mcp_resource_agent_read_enabled', True),
|
||||
'bound_skills': prefs.get('skills', []),
|
||||
'available_skills': await self.ap.skill_service.list_skills(context),
|
||||
}
|
||||
|
||||
async def update_pipeline_extensions(
|
||||
self,
|
||||
context: TenantContext,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Single-turn diagnostic execution through the normal Pipeline scheduler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from langbot_plugin.api.entities.builtin.platform import entities, events, message
|
||||
from langbot_plugin.api.entities.builtin.provider.session import LauncherTypes
|
||||
|
||||
from ..authz import Permission, require_permission
|
||||
from ..context import ExecutionContext, RequestContext
|
||||
from ....workspace.errors import WorkspaceNotFoundError
|
||||
|
||||
|
||||
async def run_pipeline(ap, context: RequestContext, pipeline_uuid: str, text: str, *, timeout: float = 60) -> dict:
|
||||
"""Run in a fresh session; a timed-out request is never submitted again."""
|
||||
require_permission(context, Permission.RUNTIME_OPERATE)
|
||||
if not isinstance(text, str) or not text.strip() or len(text) > 100_000:
|
||||
raise ValueError('message must contain 1..100000 characters')
|
||||
if await ap.pipeline_service.get_pipeline(context, pipeline_uuid) is None:
|
||||
raise WorkspaceNotFoundError('Pipeline not found')
|
||||
|
||||
bot = await ap.platform_mgr.get_websocket_proxy_bot(context)
|
||||
adapter = bot.adapter
|
||||
session_id = str(uuid.uuid4())
|
||||
launcher_id = f'websocket_{pipeline_uuid}:{session_id}'
|
||||
chain = message.MessageChain([message.Plain(text=text)])
|
||||
event = events.FriendMessage(
|
||||
sender=entities.Friend(id=launcher_id, nickname='CLI', remark='CLI'),
|
||||
message_chain=chain,
|
||||
time=time.time(),
|
||||
)
|
||||
execution = ExecutionContext.from_request(context, bot_uuid='websocket-proxy-bot', pipeline_uuid=pipeline_uuid)
|
||||
query = await ap.query_pool.add_query(
|
||||
bot_uuid='websocket-proxy-bot',
|
||||
launcher_type=LauncherTypes.PERSON,
|
||||
launcher_id=launcher_id,
|
||||
sender_id=launcher_id,
|
||||
message_event=event,
|
||||
message_chain=chain,
|
||||
adapter=adapter,
|
||||
pipeline_uuid=pipeline_uuid,
|
||||
variables={'_cli_run_status': 'failed'},
|
||||
execution_context=execution,
|
||||
)
|
||||
completed = asyncio.Event()
|
||||
# add_query returns without yielding after enqueueing, so the scheduler
|
||||
# cannot remove this query before its completion notification is attached.
|
||||
object.__setattr__(query, '_completion_event', completed)
|
||||
data = {'pipeline_uuid': pipeline_uuid, 'session_id': f'person_{launcher_id}', 'query_id': query.query_uuid}
|
||||
try:
|
||||
await asyncio.wait_for(completed.wait(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
return {**data, 'status': 'unknown', 'error': 'Execution status unknown; do not automatically retry.'}
|
||||
|
||||
replies = adapter.get_websocket_messages(pipeline_uuid, 'person', session_id)
|
||||
replies = [reply for reply in replies if reply.get('role') == 'assistant']
|
||||
data.update(
|
||||
status=query.variables['_cli_run_status'],
|
||||
replies=replies,
|
||||
reply='\n'.join(reply.get('content', '') for reply in replies),
|
||||
message_id=query.variables.get('_monitoring_message_id'),
|
||||
)
|
||||
if data['status'] != 'completed':
|
||||
data['error'] = 'Pipeline failed or was dropped; inspect the message and call records.'
|
||||
return data
|
||||
@@ -96,6 +96,119 @@ class LangBotMCPServer:
|
||||
}
|
||||
return _dump(data)
|
||||
|
||||
@mcp.tool(description='List knowledge engines and their capabilities.')
|
||||
async def list_knowledge_engines() -> str:
|
||||
return _dump(await ap.knowledge_service.list_knowledge_engines(_authorized(Permission.RESOURCE_VIEW)))
|
||||
|
||||
@mcp.tool(description='Get creation or retrieval schema for an author/name knowledge engine.')
|
||||
async def get_knowledge_engine_schema(plugin_id: str, kind: typing.Literal['creation', 'retrieval']) -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
if kind == 'creation':
|
||||
return _dump(await ap.knowledge_service.get_engine_creation_schema(context, plugin_id))
|
||||
return _dump(await ap.knowledge_service.get_engine_retrieval_schema(context, plugin_id))
|
||||
|
||||
@mcp.tool(description='List document parsers, optionally filtered by MIME type.')
|
||||
async def list_knowledge_parsers(mime_type: str | None = None) -> str:
|
||||
return _dump(await ap.knowledge_service.list_parsers(_authorized(Permission.RESOURCE_VIEW), mime_type))
|
||||
|
||||
@mcp.tool(description='Read Pipeline extension bindings, enable-all switches and available extensions.')
|
||||
async def get_pipeline_extensions(pipeline_uuid: str) -> str:
|
||||
return _dump(
|
||||
await ap.pipeline_service.get_pipeline_extensions(_authorized(Permission.RESOURCE_VIEW), pipeline_uuid)
|
||||
)
|
||||
|
||||
@mcp.tool(description='Replace all Pipeline extension bindings. All binding lists and switches are required.')
|
||||
async def update_pipeline_extensions(
|
||||
pipeline_uuid: str,
|
||||
bound_plugins: list[dict],
|
||||
bound_mcp_servers: list[str],
|
||||
bound_skills: list[str],
|
||||
bound_mcp_resources: list[dict],
|
||||
enable_all_plugins: bool,
|
||||
enable_all_mcp_servers: bool,
|
||||
enable_all_skills: bool,
|
||||
mcp_resource_agent_read_enabled: bool,
|
||||
) -> str:
|
||||
context = _authorized(Permission.RESOURCE_MANAGE)
|
||||
require_permission(context, Permission.RESOURCE_VIEW)
|
||||
await ap.pipeline_service.update_pipeline_extensions(
|
||||
context,
|
||||
pipeline_uuid,
|
||||
bound_plugins,
|
||||
bound_mcp_servers,
|
||||
enable_all_plugins,
|
||||
enable_all_mcp_servers,
|
||||
bound_skills,
|
||||
enable_all_skills,
|
||||
bound_mcp_resources,
|
||||
mcp_resource_agent_read_enabled,
|
||||
)
|
||||
return _dump(await ap.pipeline_service.get_pipeline_extensions(context, pipeline_uuid))
|
||||
|
||||
@mcp.tool(
|
||||
description='Run one Pipeline turn in a fresh session. Calls configured models/tools. Never retry an unknown outcome automatically.'
|
||||
)
|
||||
async def run_pipeline(pipeline_uuid: str, message: str) -> str:
|
||||
from ..http.service.pipeline_run import run_pipeline as execute
|
||||
|
||||
return _dump(await execute(ap, _authorized(Permission.RUNTIME_OPERATE), pipeline_uuid, message))
|
||||
|
||||
@mcp.tool(
|
||||
description='Read Workspace sandbox status, sessions or recent errors. Does not create execution sessions.'
|
||||
)
|
||||
async def get_sandbox_diagnostics(kind: typing.Literal['status', 'sessions', 'errors'] = 'status') -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW if kind == 'status' else Permission.AUDIT_VIEW)
|
||||
if kind == 'status':
|
||||
return _dump(await ap.box_service.get_status(context))
|
||||
if kind == 'sessions':
|
||||
return _dump(await ap.box_service.get_sessions(context))
|
||||
if ap.box_service.managed_admission_required:
|
||||
await ap.box_service.require_workspace_sandbox(context)
|
||||
return _dump(ap.box_service.get_recent_errors(context))
|
||||
|
||||
@mcp.tool(
|
||||
description='Read bounded Workspace runtime records. Filters follow the monitoring service: bot_ids, pipeline_ids, session_ids, start_time, end_time, knowledge_base_id, user_query, is_active as supported by the record kind.'
|
||||
)
|
||||
async def get_monitoring_records(
|
||||
kind: typing.Literal['messages', 'llm_calls', 'tool_calls', 'embedding_calls', 'sessions', 'errors'],
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
filters: dict | None = None,
|
||||
) -> str:
|
||||
import datetime
|
||||
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
allowed = {'start_time', 'end_time'}
|
||||
if kind != 'embedding_calls':
|
||||
allowed |= {'bot_ids', 'pipeline_ids'}
|
||||
if kind in {'messages', 'tool_calls'}:
|
||||
allowed.add('session_ids')
|
||||
if kind == 'embedding_calls':
|
||||
allowed.add('knowledge_base_id')
|
||||
if kind == 'sessions':
|
||||
allowed |= {'user_query', 'is_active'}
|
||||
kwargs = dict(filters or {})
|
||||
if set(kwargs) - allowed:
|
||||
raise ValueError('Unsupported monitoring filter')
|
||||
for field in ('start_time', 'end_time'):
|
||||
if kwargs.get(field):
|
||||
value = datetime.datetime.fromisoformat(kwargs[field].replace('Z', '+00:00'))
|
||||
if value.tzinfo is not None:
|
||||
value = value.astimezone(datetime.timezone.utc).replace(tzinfo=None)
|
||||
kwargs[field] = value
|
||||
limit, offset = ap.monitoring_service.normalize_page_window(limit, offset)
|
||||
rows, total = await getattr(ap.monitoring_service, 'get_' + kind)(
|
||||
context, limit=limit, offset=offset, **kwargs
|
||||
)
|
||||
return _dump({kind: rows, 'total': total, 'limit': limit, 'offset': offset})
|
||||
|
||||
@mcp.tool(description='Read message details or session analysis within the authenticated Workspace.')
|
||||
async def get_monitoring_details(kind: typing.Literal['message', 'session'], identifier: str) -> str:
|
||||
context = _authorized(Permission.RESOURCE_VIEW)
|
||||
if kind == 'message':
|
||||
return _dump(await ap.monitoring_service.get_message_details(context, identifier))
|
||||
return _dump(await ap.monitoring_service.get_session_analysis(context, identifier))
|
||||
|
||||
# ----- Bots ---------------------------------------------------- #
|
||||
@mcp.tool(description='List all messaging-platform bots. Secrets are redacted.')
|
||||
async def list_bots() -> str:
|
||||
|
||||
@@ -1373,13 +1373,14 @@ class BoxService:
|
||||
self._runtime_connector.dispose()
|
||||
|
||||
async def get_sessions(self, context: TenantContext) -> list[dict]:
|
||||
if not self._available:
|
||||
if not self._enabled:
|
||||
if self.managed_admission_required:
|
||||
await self.require_workspace_sandbox(context)
|
||||
return []
|
||||
execution_context = await self.require_workspace_sandbox(context)
|
||||
try:
|
||||
return await self.client.get_sessions(action_context=self._action_context(execution_context))
|
||||
except Exception:
|
||||
return []
|
||||
if not self._available:
|
||||
raise RuntimeError('Sandbox is unavailable; inspect sandbox status')
|
||||
return await self.client.get_sessions(action_context=self._action_context(execution_context))
|
||||
|
||||
async def get_storage_analysis(self, context: TenantContext) -> dict:
|
||||
"""Return Workspace-scoped storage measured by the Box Runtime."""
|
||||
|
||||
@@ -444,6 +444,10 @@ class RuntimePipeline:
|
||||
self.ap.logger.debug(f'Processing query {query.query_id}')
|
||||
|
||||
await self._execute_from_stage(0, query)
|
||||
if '_cli_run_status' in query.variables:
|
||||
query.variables['_cli_run_status'] = (
|
||||
'failed' if query.variables.get('_monitoring_has_error') else 'completed'
|
||||
)
|
||||
|
||||
# Record query success only if no error occurred during processing
|
||||
if not query.variables.get('_monitoring_has_error', False):
|
||||
|
||||
@@ -173,6 +173,9 @@ class QueryPool:
|
||||
else:
|
||||
self.active_query_count_by_workspace.pop(query_workspace_uuid, None)
|
||||
plugin_diagnostics.discard_query_state(query)
|
||||
completion = getattr(query, '_completion_event', None)
|
||||
if completion is not None:
|
||||
completion.set()
|
||||
counter_key = (
|
||||
execution_context.instance_uuid,
|
||||
execution_context.workspace_uuid,
|
||||
@@ -426,6 +429,9 @@ class QueryPool:
|
||||
self.queries.pop(index)
|
||||
break
|
||||
plugin_diagnostics.discard_query_state(query)
|
||||
completion = getattr(query, '_completion_event', None)
|
||||
if completion is not None:
|
||||
completion.set()
|
||||
return True
|
||||
|
||||
async def __aenter__(self):
|
||||
|
||||
Reference in New Issue
Block a user