fix(assistant): align resource tools with application services

This commit is contained in:
huanghuoguoguo
2026-09-16 22:37:02 +08:00
parent e82d71029e
commit 48140fbfd4
4 changed files with 44 additions and 6 deletions
@@ -253,7 +253,8 @@ class AssistantService:
await self._save(context, conversation, 'running') await self._save(context, conversation, 'running')
try: try:
result = await execute_tool(self.ap, context, func['name'], json.loads(func['arguments'] or '{}')) 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( self._append_result(
conversation, conversation,
call, call,
@@ -89,13 +89,18 @@ async def execute_tool(ap, context, name: str, arguments: dict):
args = validate_call(context, name, arguments).model_dump(mode='json') args = validate_call(context, name, arguments).model_dump(mode='json')
if name == 'list_resources': if name == 'list_resources':
readers = { readers = {
'models': ap.model_service.get_llm_models, 'models': ap.llm_model_service.get_llm_models,
'embedding_models': ap.model_service.get_embedding_models, 'embedding_models': ap.embedding_models_service.get_embedding_models,
'pipelines': ap.pipeline_service.get_pipelines, 'pipelines': ap.pipeline_service.get_pipelines,
'knowledge_bases': ap.knowledge_service.get_knowledge_bases, 'knowledge_bases': ap.knowledge_service.get_knowledge_bases,
'knowledge_engines': ap.knowledge_service.list_knowledge_engines, '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': if name == 'get_pipeline':
return await ap.pipeline_service.get_pipeline(context, args['pipeline_uuid']) return await ap.pipeline_service.get_pipeline(context, args['pipeline_uuid'])
if name == 'get_knowledge_schema': if name == 'get_knowledge_schema':
@@ -3,7 +3,7 @@
import asyncio import asyncio
import logging import logging
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import AsyncMock from unittest.mock import AsyncMock, create_autospec
import pytest import pytest
import sqlalchemy as sa 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.authz import Permission
from langbot.pkg.api.http.context import RequestContext, PrincipalContext, PrincipalType, WorkspaceContext 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 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 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'}) validate_call(context(), 'create_pipeline', {'name': 'test', 'workspace_uuid': 'other'})
with pytest.raises(ValueError): with pytest.raises(ValueError):
validate_call(context(), 'exec', {'command': 'echo unsafe'}) 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)
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { MessageCircle, Plus, Send, X, LoaderCircle } from 'lucide-react'; import { MessageCircle, Plus, Send, X, LoaderCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { backendClient, useCurrentWorkspace, userInfo } from '@/app/infra/http'; import { backendClient, useCurrentWorkspace, userInfo } from '@/app/infra/http';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { 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'}`} className={`rounded-xl p-3 text-sm break-words ${message.role === 'user' ? 'ml-6 bg-primary/10' : 'bg-muted'}`}
> >
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{ components={{
img: () => null, img: () => null,
a: ({ href, children }) => ( a: ({ href, children }) => (