diff --git a/src/langbot/pkg/api/http/service/assistant.py b/src/langbot/pkg/api/http/service/assistant.py index a1f77895b..80d171ed6 100644 --- a/src/langbot/pkg/api/http/service/assistant.py +++ b/src/langbot/pkg/api/http/service/assistant.py @@ -253,7 +253,8 @@ class AssistantService: await self._save(context, conversation, 'running') try: result = await execute_tool(self.ap, context, func['name'], json.loads(func['arguments'] or '{}')) - except Exception: + except Exception as exc: + self.ap.logger.warning('Management assistant tool %s failed (%s)', func['name'], type(exc).__name__) self._append_result( conversation, call, diff --git a/src/langbot/pkg/api/http/service/assistant_tools.py b/src/langbot/pkg/api/http/service/assistant_tools.py index a3bf965af..f5d468194 100644 --- a/src/langbot/pkg/api/http/service/assistant_tools.py +++ b/src/langbot/pkg/api/http/service/assistant_tools.py @@ -89,13 +89,18 @@ async def execute_tool(ap, context, name: str, arguments: dict): args = validate_call(context, name, arguments).model_dump(mode='json') if name == 'list_resources': readers = { - 'models': ap.model_service.get_llm_models, - 'embedding_models': ap.model_service.get_embedding_models, + 'models': ap.llm_model_service.get_llm_models, + 'embedding_models': ap.embedding_models_service.get_embedding_models, 'pipelines': ap.pipeline_service.get_pipelines, 'knowledge_bases': ap.knowledge_service.get_knowledge_bases, 'knowledge_engines': ap.knowledge_service.list_knowledge_engines, } - return redact_secrets(await readers[args['kind']](context)) + resources = await readers[args['kind']](context) + if args['kind'] != 'knowledge_engines': + # Lists discover resources; get_pipeline / get_knowledge_schema supply configuration details. + fields = ('uuid', 'name', 'description', 'abilities', 'knowledge_engine_plugin_id') + resources = [{key: item[key] for key in fields if key in item} for item in resources] + return {'total': len(resources), 'items': redact_secrets(resources)} if name == 'get_pipeline': return await ap.pipeline_service.get_pipeline(context, args['pipeline_uuid']) if name == 'get_knowledge_schema': diff --git a/tests/unit_tests/api/test_management_assistant.py b/tests/unit_tests/api/test_management_assistant.py index 292f127d2..58669a042 100644 --- a/tests/unit_tests/api/test_management_assistant.py +++ b/tests/unit_tests/api/test_management_assistant.py @@ -3,7 +3,7 @@ import asyncio import logging from types import SimpleNamespace -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, create_autospec import pytest import sqlalchemy as sa @@ -13,7 +13,7 @@ from langbot_plugin.api.entities.builtin.provider.message import Message from langbot.pkg.api.http.authz import Permission from langbot.pkg.api.http.context import RequestContext, PrincipalContext, PrincipalType, WorkspaceContext from langbot.pkg.api.http.service.assistant import AssistantError, AssistantService -from langbot.pkg.api.http.service.assistant_tools import validate_call +from langbot.pkg.api.http.service.assistant_tools import execute_tool, validate_call from langbot.pkg.entity.persistence.assistant import AssistantConversation @@ -145,3 +145,33 @@ def test_tool_arguments_cannot_select_identity_or_shell(): validate_call(context(), 'create_pipeline', {'name': 'test', 'workspace_uuid': 'other'}) with pytest.raises(ValueError): validate_call(context(), 'exec', {'command': 'echo unsafe'}) + + +@pytest.mark.asyncio +async def test_resource_readers_match_application_services(): + from langbot.pkg.core.app import Application + from langbot.pkg.api.http.service.model import LLMModelsService, EmbeddingModelsService + from langbot.pkg.api.http.service.pipeline import PipelineService + from langbot.pkg.api.http.service.knowledge import KnowledgeService + + ap = create_autospec(Application, instance=True, spec_set=True) + ap.llm_model_service = create_autospec(LLMModelsService, instance=True) + ap.embedding_models_service = create_autospec(EmbeddingModelsService, instance=True) + ap.pipeline_service = create_autospec(PipelineService, instance=True) + ap.knowledge_service = create_autospec(KnowledgeService, instance=True) + ctx = context() + for kind, reader in ( + ('models', ap.llm_model_service.get_llm_models), + ('embedding_models', ap.embedding_models_service.get_embedding_models), + ('pipelines', ap.pipeline_service.get_pipelines), + ('knowledge_bases', ap.knowledge_service.get_knowledge_bases), + ('knowledge_engines', ap.knowledge_service.list_knowledge_engines), + ): + reader.return_value = [{'name': kind}] + if kind != 'knowledge_engines': + reader.return_value[0]['config'] = {'large_or_private': 'omitted from discovery'} + assert await execute_tool(ap, ctx, 'list_resources', {'kind': kind}) == { + 'total': 1, + 'items': [{'name': kind}], + } + reader.assert_awaited_once_with(ctx) diff --git a/web/src/app/home/components/WorkspaceAssistant.tsx b/web/src/app/home/components/WorkspaceAssistant.tsx index 651c4f4b0..42242e20f 100644 --- a/web/src/app/home/components/WorkspaceAssistant.tsx +++ b/web/src/app/home/components/WorkspaceAssistant.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { MessageCircle, Plus, Send, X, LoaderCircle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import { backendClient, useCurrentWorkspace, userInfo } from '@/app/infra/http'; import { Button } from '@/components/ui/button'; import { @@ -230,6 +231,7 @@ function AssistantPanel({ storageKey }: { storageKey: string }) { className={`rounded-xl p-3 text-sm break-words ${message.role === 'user' ? 'ml-6 bg-primary/10' : 'bg-muted'}`} > null, a: ({ href, children }) => (