mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 03:15:06 +08:00
Compare commits
7 Commits
c3c51b0fbf
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
318c6b6c66 | ||
|
|
00a7e86875 | ||
|
|
abe97957b4 | ||
|
|
1a07ed6e99 | ||
|
|
64fd4b55c7 | ||
|
|
5e81306d7c | ||
|
|
6c5105b61f |
@@ -63,7 +63,7 @@ dependencies = [
|
||||
"langchain-text-splitters>=0.0.1",
|
||||
"chromadb>=0.4.24",
|
||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||
"langbot-plugin==0.1.11",
|
||||
"langbot-plugin==0.1.12",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"tboxsdk>=0.0.10",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import base, external
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import quart
|
||||
from ... import group
|
||||
|
||||
|
||||
@group.group_class('external_knowledge_base', '/api/v1/knowledge/external-bases')
|
||||
class ExternalKnowledgeBaseRouterGroup(group.RouterGroup):
|
||||
async def initialize(self) -> None:
|
||||
@self.route('', methods=['POST', 'GET'])
|
||||
async def handle_external_knowledge_bases() -> quart.Response:
|
||||
if quart.request.method == 'GET':
|
||||
external_kbs = await self.ap.knowledge_service.get_external_knowledge_bases()
|
||||
return self.success(data={'bases': external_kbs})
|
||||
|
||||
elif quart.request.method == 'POST':
|
||||
json_data = await quart.request.json
|
||||
kb_uuid = await self.ap.knowledge_service.create_external_knowledge_base(json_data)
|
||||
return self.success(data={'uuid': kb_uuid})
|
||||
|
||||
return self.http_status(405, -1, 'Method not allowed')
|
||||
|
||||
@self.route(
|
||||
'/<kb_uuid>',
|
||||
methods=['GET', 'DELETE', 'PUT'],
|
||||
)
|
||||
async def handle_specific_external_knowledge_base(kb_uuid: str) -> quart.Response:
|
||||
if quart.request.method == 'GET':
|
||||
external_kb = await self.ap.knowledge_service.get_external_knowledge_base(kb_uuid)
|
||||
|
||||
if external_kb is None:
|
||||
return self.http_status(404, -1, 'external knowledge base not found')
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
'base': external_kb,
|
||||
}
|
||||
)
|
||||
|
||||
elif quart.request.method == 'PUT':
|
||||
json_data = await quart.request.json
|
||||
await self.ap.knowledge_service.update_external_knowledge_base(kb_uuid, json_data)
|
||||
return self.success({})
|
||||
|
||||
elif quart.request.method == 'DELETE':
|
||||
await self.ap.knowledge_service.delete_external_knowledge_base(kb_uuid)
|
||||
return self.success({})
|
||||
|
||||
@self.route(
|
||||
'/<kb_uuid>/retrieve',
|
||||
methods=['POST'],
|
||||
)
|
||||
async def retrieve_external_knowledge_base(kb_uuid: str) -> str:
|
||||
json_data = await quart.request.json
|
||||
query = json_data.get('query')
|
||||
results = await self.ap.knowledge_service.retrieve_knowledge_base(kb_uuid, query)
|
||||
return self.success(data={'results': results})
|
||||
@@ -71,6 +71,9 @@ class KnowledgeService:
|
||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
||||
if runtime_kb is None:
|
||||
raise Exception('Knowledge base not found')
|
||||
# Only internal KBs support file storage
|
||||
if runtime_kb.get_type() != 'internal':
|
||||
raise Exception('Only internal knowledge bases support file storage')
|
||||
return await runtime_kb.store_file(file_id)
|
||||
|
||||
async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]:
|
||||
@@ -78,9 +81,16 @@ class KnowledgeService:
|
||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
||||
if runtime_kb is None:
|
||||
raise Exception('Knowledge base not found')
|
||||
return [
|
||||
result.model_dump() for result in await runtime_kb.retrieve(query, runtime_kb.knowledge_base_entity.top_k)
|
||||
]
|
||||
|
||||
# Get top_k based on KB type
|
||||
if runtime_kb.get_type() == 'internal':
|
||||
top_k = runtime_kb.knowledge_base_entity.top_k
|
||||
elif runtime_kb.get_type() == 'external':
|
||||
top_k = runtime_kb.external_kb_entity.top_k
|
||||
else:
|
||||
top_k = 5 # default fallback
|
||||
|
||||
return [result.model_dump() for result in await runtime_kb.retrieve(query, top_k)]
|
||||
|
||||
async def get_files_by_knowledge_base(self, kb_uuid: str) -> list[dict]:
|
||||
"""获取知识库文件"""
|
||||
@@ -95,6 +105,9 @@ class KnowledgeService:
|
||||
runtime_kb = await self.ap.rag_mgr.get_knowledge_base_by_uuid(kb_uuid)
|
||||
if runtime_kb is None:
|
||||
raise Exception('Knowledge base not found')
|
||||
# Only internal KBs support file deletion
|
||||
if runtime_kb.get_type() != 'internal':
|
||||
raise Exception('Only internal knowledge bases support file deletion')
|
||||
await runtime_kb.delete_file(file_id)
|
||||
|
||||
async def delete_knowledge_base(self, kb_uuid: str) -> None:
|
||||
@@ -118,3 +131,66 @@ class KnowledgeService:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.delete(persistence_rag.File).where(persistence_rag.File.uuid == file.uuid)
|
||||
)
|
||||
|
||||
# External Knowledge Base methods
|
||||
async def get_external_knowledge_bases(self) -> list[dict]:
|
||||
"""获取所有外部知识库"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_rag.ExternalKnowledgeBase)
|
||||
)
|
||||
external_kbs = result.all()
|
||||
return [
|
||||
self.ap.persistence_mgr.serialize_model(persistence_rag.ExternalKnowledgeBase, external_kb)
|
||||
for external_kb in external_kbs
|
||||
]
|
||||
|
||||
async def get_external_knowledge_base(self, kb_uuid: str) -> dict | None:
|
||||
"""获取外部知识库"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_rag.ExternalKnowledgeBase).where(
|
||||
persistence_rag.ExternalKnowledgeBase.uuid == kb_uuid
|
||||
)
|
||||
)
|
||||
external_kb = result.first()
|
||||
if external_kb is None:
|
||||
return None
|
||||
return self.ap.persistence_mgr.serialize_model(persistence_rag.ExternalKnowledgeBase, external_kb)
|
||||
|
||||
async def create_external_knowledge_base(self, kb_data: dict) -> str:
|
||||
"""创建外部知识库"""
|
||||
kb_data['uuid'] = str(uuid.uuid4())
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.insert(persistence_rag.ExternalKnowledgeBase).values(kb_data)
|
||||
)
|
||||
|
||||
kb = await self.get_external_knowledge_base(kb_data['uuid'])
|
||||
|
||||
await self.ap.rag_mgr.load_external_knowledge_base(kb)
|
||||
|
||||
return kb_data['uuid']
|
||||
|
||||
async def update_external_knowledge_base(self, kb_uuid: str, kb_data: dict) -> None:
|
||||
"""更新外部知识库"""
|
||||
if 'uuid' in kb_data:
|
||||
del kb_data['uuid']
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(persistence_rag.ExternalKnowledgeBase)
|
||||
.values(kb_data)
|
||||
.where(persistence_rag.ExternalKnowledgeBase.uuid == kb_uuid)
|
||||
)
|
||||
await self.ap.rag_mgr.remove_knowledge_base_from_runtime(kb_uuid)
|
||||
|
||||
kb = await self.get_external_knowledge_base(kb_uuid)
|
||||
|
||||
await self.ap.rag_mgr.load_external_knowledge_base(kb)
|
||||
|
||||
async def delete_external_knowledge_base(self, kb_uuid: str) -> None:
|
||||
"""删除外部知识库"""
|
||||
await self.ap.rag_mgr.delete_knowledge_base(kb_uuid)
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.delete(persistence_rag.ExternalKnowledgeBase).where(
|
||||
persistence_rag.ExternalKnowledgeBase.uuid == kb_uuid
|
||||
)
|
||||
)
|
||||
|
||||
@@ -43,6 +43,17 @@ class Chunk(Base):
|
||||
text = sqlalchemy.Column(sqlalchemy.Text)
|
||||
|
||||
|
||||
class ExternalKnowledgeBase(Base):
|
||||
__tablename__ = 'external_knowledge_bases'
|
||||
uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
name = sqlalchemy.Column(sqlalchemy.String, index=True)
|
||||
description = sqlalchemy.Column(sqlalchemy.Text)
|
||||
api_url = sqlalchemy.Column(sqlalchemy.String, nullable=False)
|
||||
api_key = sqlalchemy.Column(sqlalchemy.String, nullable=True)
|
||||
created_at = sqlalchemy.Column(sqlalchemy.DateTime, default=sqlalchemy.func.now())
|
||||
top_k = sqlalchemy.Column(sqlalchemy.Integer, default=5)
|
||||
|
||||
|
||||
# class Vector(Base):
|
||||
# __tablename__ = 'knowledge_base_vectors'
|
||||
# uuid = sqlalchemy.Column(sqlalchemy.String(255), primary_key=True, unique=True)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pydantic
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class RetrieveResultEntry(pydantic.BaseModel):
|
||||
id: str
|
||||
|
||||
metadata: dict[str, Any]
|
||||
|
||||
distance: float
|
||||
@@ -73,7 +73,15 @@ class LocalAgentRunner(runner.RequestRunner):
|
||||
self.ap.logger.warning(f'Knowledge base {kb_uuid} not found, skipping')
|
||||
continue
|
||||
|
||||
result = await kb.retrieve(user_message_text, kb.knowledge_base_entity.top_k)
|
||||
# Get top_k based on KB type
|
||||
if kb.get_type() == 'internal':
|
||||
top_k = kb.knowledge_base_entity.top_k
|
||||
elif kb.get_type() == 'external':
|
||||
top_k = kb.external_kb_entity.top_k
|
||||
else:
|
||||
top_k = 5 # default fallback
|
||||
|
||||
result = await kb.retrieve(user_message_text, top_k)
|
||||
|
||||
if result:
|
||||
all_results.extend(result)
|
||||
|
||||
55
src/langbot/pkg/rag/knowledge/base.py
Normal file
55
src/langbot/pkg/rag/knowledge/base.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Base classes and interfaces for knowledge bases"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
|
||||
from langbot.pkg.core import app
|
||||
from langbot_plugin.api.entities.rag import context as rag_context
|
||||
|
||||
|
||||
class KnowledgeBaseInterface(metaclass=abc.ABCMeta):
|
||||
"""Abstract interface for all knowledge base types"""
|
||||
|
||||
ap: app.Application
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
@abc.abstractmethod
|
||||
async def initialize(self):
|
||||
"""Initialize the knowledge base"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def retrieve(self, query: str, top_k: int) -> list[rag_context.RetrievalResultEntry]:
|
||||
"""Retrieve relevant documents from the knowledge base
|
||||
|
||||
Args:
|
||||
query: The query string
|
||||
top_k: Number of top results to return
|
||||
|
||||
Returns:
|
||||
List of retrieve result entries
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_uuid(self) -> str:
|
||||
"""Get the UUID of the knowledge base"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_name(self) -> str:
|
||||
"""Get the name of the knowledge base"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_type(self) -> str:
|
||||
"""Get the type of knowledge base (internal/external)"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def dispose(self):
|
||||
"""Clean up resources"""
|
||||
pass
|
||||
123
src/langbot/pkg/rag/knowledge/external.py
Normal file
123
src/langbot/pkg/rag/knowledge/external.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""External knowledge base implementation"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
|
||||
from langbot.pkg.core import app
|
||||
from langbot.pkg.entity.persistence import rag as persistence_rag
|
||||
from langbot_plugin.api.entities.rag import context as rag_context
|
||||
from .base import KnowledgeBaseInterface
|
||||
|
||||
|
||||
class ExternalKnowledgeBase(KnowledgeBaseInterface):
|
||||
"""External knowledge base that queries via HTTP API"""
|
||||
|
||||
external_kb_entity: persistence_rag.ExternalKnowledgeBase
|
||||
|
||||
def __init__(self, ap: app.Application, external_kb_entity: persistence_rag.ExternalKnowledgeBase):
|
||||
super().__init__(ap)
|
||||
self.external_kb_entity = external_kb_entity
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the external knowledge base"""
|
||||
pass
|
||||
|
||||
async def retrieve(self, query: str, top_k: int) -> list[rag_context.RetrievalResultEntry]:
|
||||
"""Retrieve documents from external knowledge base via HTTP API
|
||||
|
||||
The API should follow this format:
|
||||
POST {api_url}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {api_key} (if api_key is provided)
|
||||
|
||||
Request body:
|
||||
{
|
||||
"query": "user query text",
|
||||
"top_k": 5
|
||||
}
|
||||
|
||||
Response format:
|
||||
{
|
||||
"records": [
|
||||
{
|
||||
"content": "document text content",
|
||||
"score": 0.95,
|
||||
"title": "optional document title",
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
if self.external_kb_entity.api_key:
|
||||
headers['Authorization'] = f'Bearer {self.external_kb_entity.api_key}'
|
||||
|
||||
request_data = {'query': query, 'top_k': top_k}
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.post(
|
||||
self.external_kb_entity.api_url, json=request_data, headers=headers
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
self.ap.logger.error(f'External KB API error: status={response.status}, body={error_text}')
|
||||
return []
|
||||
|
||||
response_data = await response.json()
|
||||
|
||||
# Parse response
|
||||
records = response_data.get('records', [])
|
||||
results = []
|
||||
|
||||
for record in records:
|
||||
content = record.get('content', '')
|
||||
score = record.get('score', 0.0)
|
||||
title = record.get('title', '')
|
||||
metadata = record.get('metadata', {})
|
||||
|
||||
# Build metadata for result
|
||||
result_metadata = {
|
||||
'text': content,
|
||||
'score': score,
|
||||
'source': 'external_kb',
|
||||
'kb_uuid': self.external_kb_entity.uuid,
|
||||
'kb_name': self.external_kb_entity.name,
|
||||
}
|
||||
|
||||
if title:
|
||||
result_metadata['title'] = title
|
||||
|
||||
# Merge additional metadata
|
||||
result_metadata.update(metadata)
|
||||
|
||||
results.append(rag_context.RetrievalResultEntry(score=score, metadata=result_metadata))
|
||||
|
||||
return results
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
self.ap.logger.error(f'External KB HTTP error: {e}')
|
||||
return []
|
||||
except Exception as e:
|
||||
self.ap.logger.error(f'External KB retrieval error: {e}')
|
||||
return []
|
||||
|
||||
def get_uuid(self) -> str:
|
||||
"""Get the UUID of the external knowledge base"""
|
||||
return self.external_kb_entity.uuid
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Get the name of the external knowledge base"""
|
||||
return self.external_kb_entity.name
|
||||
|
||||
def get_type(self) -> str:
|
||||
"""Get the type of knowledge base"""
|
||||
return 'external'
|
||||
|
||||
async def dispose(self):
|
||||
"""Clean up resources - no cleanup needed for external KB"""
|
||||
pass
|
||||
@@ -10,10 +10,12 @@ from langbot.pkg.rag.knowledge.services.retriever import Retriever
|
||||
import sqlalchemy
|
||||
from langbot.pkg.entity.persistence import rag as persistence_rag
|
||||
from langbot.pkg.core import taskmgr
|
||||
from langbot.pkg.entity.rag import retriever as retriever_entities
|
||||
from langbot_plugin.api.entities.rag import context as rag_context
|
||||
from .base import KnowledgeBaseInterface
|
||||
from .external import ExternalKnowledgeBase
|
||||
|
||||
|
||||
class RuntimeKnowledgeBase:
|
||||
class RuntimeKnowledgeBase(KnowledgeBaseInterface):
|
||||
ap: app.Application
|
||||
|
||||
knowledge_base_entity: persistence_rag.KnowledgeBase
|
||||
@@ -27,7 +29,7 @@ class RuntimeKnowledgeBase:
|
||||
retriever: Retriever
|
||||
|
||||
def __init__(self, ap: app.Application, knowledge_base_entity: persistence_rag.KnowledgeBase):
|
||||
self.ap = ap
|
||||
super().__init__(ap)
|
||||
self.knowledge_base_entity = knowledge_base_entity
|
||||
self.parser = parser.FileParser(ap=self.ap)
|
||||
self.chunker = chunker.Chunker(ap=self.ap)
|
||||
@@ -187,7 +189,7 @@ class RuntimeKnowledgeBase:
|
||||
|
||||
return stored_file_tasks[0] if stored_file_tasks else ''
|
||||
|
||||
async def retrieve(self, query: str, top_k: int) -> list[retriever_entities.RetrieveResultEntry]:
|
||||
async def retrieve(self, query: str, top_k: int) -> list[rag_context.RetrievalResultEntry]:
|
||||
embedding_model = await self.ap.model_mgr.get_embedding_model_by_uuid(
|
||||
self.knowledge_base_entity.embedding_model_uuid
|
||||
)
|
||||
@@ -206,6 +208,18 @@ class RuntimeKnowledgeBase:
|
||||
sqlalchemy.delete(persistence_rag.File).where(persistence_rag.File.uuid == file_id)
|
||||
)
|
||||
|
||||
def get_uuid(self) -> str:
|
||||
"""Get the UUID of the knowledge base"""
|
||||
return self.knowledge_base_entity.uuid
|
||||
|
||||
def get_name(self) -> str:
|
||||
"""Get the name of the knowledge base"""
|
||||
return self.knowledge_base_entity.name
|
||||
|
||||
def get_type(self) -> str:
|
||||
"""Get the type of knowledge base"""
|
||||
return 'internal'
|
||||
|
||||
async def dispose(self):
|
||||
await self.ap.vector_db_mgr.vector_db.delete_collection(self.knowledge_base_entity.uuid)
|
||||
|
||||
@@ -213,7 +227,7 @@ class RuntimeKnowledgeBase:
|
||||
class RAGManager:
|
||||
ap: app.Application
|
||||
|
||||
knowledge_bases: list[RuntimeKnowledgeBase]
|
||||
knowledge_bases: list[KnowledgeBaseInterface]
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
@@ -227,8 +241,8 @@ class RAGManager:
|
||||
|
||||
self.knowledge_bases = []
|
||||
|
||||
# Load internal knowledge bases
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.select(persistence_rag.KnowledgeBase))
|
||||
|
||||
knowledge_bases = result.all()
|
||||
|
||||
for knowledge_base in knowledge_bases:
|
||||
@@ -239,6 +253,20 @@ class RAGManager:
|
||||
f'Error loading knowledge base {knowledge_base.uuid}: {e}\n{traceback.format_exc()}'
|
||||
)
|
||||
|
||||
# Load external knowledge bases
|
||||
external_result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_rag.ExternalKnowledgeBase)
|
||||
)
|
||||
external_kbs = external_result.all()
|
||||
|
||||
for external_kb in external_kbs:
|
||||
try:
|
||||
await self.load_external_knowledge_base(external_kb)
|
||||
except Exception as e:
|
||||
self.ap.logger.error(
|
||||
f'Error loading external knowledge base {external_kb.uuid}: {e}\n{traceback.format_exc()}'
|
||||
)
|
||||
|
||||
async def load_knowledge_base(
|
||||
self,
|
||||
knowledge_base_entity: persistence_rag.KnowledgeBase | sqlalchemy.Row | dict,
|
||||
@@ -256,21 +284,39 @@ class RAGManager:
|
||||
|
||||
return runtime_knowledge_base
|
||||
|
||||
async def get_knowledge_base_by_uuid(self, kb_uuid: str) -> RuntimeKnowledgeBase | None:
|
||||
async def load_external_knowledge_base(
|
||||
self,
|
||||
external_kb_entity: persistence_rag.ExternalKnowledgeBase | sqlalchemy.Row | dict,
|
||||
) -> ExternalKnowledgeBase:
|
||||
"""Load external knowledge base into runtime"""
|
||||
if isinstance(external_kb_entity, sqlalchemy.Row):
|
||||
external_kb_entity = persistence_rag.ExternalKnowledgeBase(**external_kb_entity._mapping)
|
||||
elif isinstance(external_kb_entity, dict):
|
||||
external_kb_entity = persistence_rag.ExternalKnowledgeBase(**external_kb_entity)
|
||||
|
||||
external_kb = ExternalKnowledgeBase(ap=self.ap, external_kb_entity=external_kb_entity)
|
||||
|
||||
await external_kb.initialize()
|
||||
|
||||
self.knowledge_bases.append(external_kb)
|
||||
|
||||
return external_kb
|
||||
|
||||
async def get_knowledge_base_by_uuid(self, kb_uuid: str) -> KnowledgeBaseInterface | None:
|
||||
for kb in self.knowledge_bases:
|
||||
if kb.knowledge_base_entity.uuid == kb_uuid:
|
||||
if kb.get_uuid() == kb_uuid:
|
||||
return kb
|
||||
return None
|
||||
|
||||
async def remove_knowledge_base_from_runtime(self, kb_uuid: str):
|
||||
for kb in self.knowledge_bases:
|
||||
if kb.knowledge_base_entity.uuid == kb_uuid:
|
||||
if kb.get_uuid() == kb_uuid:
|
||||
self.knowledge_bases.remove(kb)
|
||||
return
|
||||
|
||||
async def delete_knowledge_base(self, kb_uuid: str):
|
||||
for kb in self.knowledge_bases:
|
||||
if kb.knowledge_base_entity.uuid == kb_uuid:
|
||||
if kb.get_uuid() == kb_uuid:
|
||||
await kb.dispose()
|
||||
self.knowledge_bases.remove(kb)
|
||||
return
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from . import base_service
|
||||
from ....core import app
|
||||
from ....provider.modelmgr.requester import RuntimeEmbeddingModel
|
||||
from ....entity.rag import retriever as retriever_entities
|
||||
from langbot_plugin.api.entities.rag import context as rag_context
|
||||
|
||||
|
||||
class Retriever(base_service.BaseService):
|
||||
@@ -13,7 +13,7 @@ class Retriever(base_service.BaseService):
|
||||
|
||||
async def retrieve(
|
||||
self, kb_id: str, query: str, embedding_model: RuntimeEmbeddingModel, k: int = 5
|
||||
) -> list[retriever_entities.RetrieveResultEntry]:
|
||||
) -> list[rag_context.RetrievalResultEntry]:
|
||||
self.ap.logger.info(
|
||||
f"Retrieving for query: '{query[:10]}' with k={k} using {embedding_model.model_entity.uuid}"
|
||||
)
|
||||
@@ -35,10 +35,10 @@ class Retriever(base_service.BaseService):
|
||||
self.ap.logger.info('No relevant chunks found in vector database.')
|
||||
return []
|
||||
|
||||
result: list[retriever_entities.RetrieveResultEntry] = []
|
||||
result: list[rag_context.RetrievalResultEntry] = []
|
||||
|
||||
for i, id in enumerate(matched_vector_ids):
|
||||
entry = retriever_entities.RetrieveResultEntry(
|
||||
entry = rag_context.RetrievalResultEntry(
|
||||
id=id,
|
||||
metadata=vector_metadatas[i],
|
||||
distance=distances[i],
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ExternalKBCardVO } from '@/app/home/knowledge/components/external-kb-card/ExternalKBCardVO';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from '../kb-card/KBCard.module.css';
|
||||
|
||||
export default function ExternalKBCard({
|
||||
kbCardVO,
|
||||
}: {
|
||||
kbCardVO: ExternalKBCardVO;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className={`${styles.cardContainer}`}>
|
||||
<div className={`${styles.basicInfoContainer}`}>
|
||||
<div className={`${styles.basicInfoNameContainer}`}>
|
||||
<div className={`${styles.basicInfoNameText} ${styles.bigText}`}>
|
||||
{kbCardVO.name}
|
||||
</div>
|
||||
<div className={`${styles.basicInfoDescriptionText}`}>
|
||||
{kbCardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.basicInfoLastUpdatedTimeContainer}`}>
|
||||
<svg
|
||||
className={`${styles.basicInfoUpdateTimeIcon}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path>
|
||||
</svg>
|
||||
<div className={`${styles.basicInfoUpdateTimeText}`}>
|
||||
{t('knowledge.updateTime')}
|
||||
{kbCardVO.lastUpdatedTimeAgo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
export class ExternalKBCardVO {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
apiUrl: string;
|
||||
top_k: number;
|
||||
lastUpdatedTimeAgo: string;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
apiUrl,
|
||||
top_k,
|
||||
lastUpdatedTimeAgo,
|
||||
}: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
apiUrl: string;
|
||||
top_k: number;
|
||||
lastUpdatedTimeAgo: string;
|
||||
}) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.apiUrl = apiUrl;
|
||||
this.top_k = top_k;
|
||||
this.lastUpdatedTimeAgo = lastUpdatedTimeAgo;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
.knowledgeListContainer {
|
||||
width: 100%;
|
||||
margin-top: 2rem;
|
||||
padding-left: 0.8rem;
|
||||
padding-right: 0.8rem;
|
||||
display: grid;
|
||||
|
||||
@@ -5,21 +5,48 @@ import styles from './knowledgeBase.module.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { KnowledgeBaseVO } from '@/app/home/knowledge/components/kb-card/KBCardVO';
|
||||
import { ExternalKBCardVO } from '@/app/home/knowledge/components/external-kb-card/ExternalKBCardVO';
|
||||
import KBCard from '@/app/home/knowledge/components/kb-card/KBCard';
|
||||
import ExternalKBCard from '@/app/home/knowledge/components/external-kb-card/ExternalKBCard';
|
||||
import KBDetailDialog from '@/app/home/knowledge/KBDetailDialog';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { KnowledgeBase } from '@/app/infra/entities/api';
|
||||
import { KnowledgeBase, ExternalKnowledgeBase } from '@/app/infra/entities/api';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function KnowledgePage() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('builtin');
|
||||
const [knowledgeBaseList, setKnowledgeBaseList] = useState<KnowledgeBaseVO[]>(
|
||||
[],
|
||||
);
|
||||
const [externalKBList, setExternalKBList] = useState<ExternalKBCardVO[]>([]);
|
||||
const [selectedKbId, setSelectedKbId] = useState<string>('');
|
||||
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
|
||||
const [externalKBDialogOpen, setExternalKBDialogOpen] = useState(false);
|
||||
const [editingExternalKB, setEditingExternalKB] =
|
||||
useState<ExternalKnowledgeBase | null>(null);
|
||||
const [externalKBForm, setExternalKBForm] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
api_url: '',
|
||||
api_key: '',
|
||||
top_k: 5,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getKnowledgeBaseList();
|
||||
getExternalKBList();
|
||||
}, []);
|
||||
|
||||
async function getKnowledgeBaseList() {
|
||||
@@ -53,6 +80,41 @@ export default function KnowledgePage() {
|
||||
);
|
||||
}
|
||||
|
||||
async function getExternalKBList() {
|
||||
try {
|
||||
const resp = await httpClient.getExternalKnowledgeBases();
|
||||
setExternalKBList(
|
||||
resp.bases.map((kb: ExternalKnowledgeBase) => {
|
||||
const currentTime = new Date();
|
||||
const lastUpdatedTimeAgo = Math.floor(
|
||||
(currentTime.getTime() -
|
||||
new Date(kb.created_at ?? currentTime.getTime()).getTime()) /
|
||||
1000 /
|
||||
60 /
|
||||
60 /
|
||||
24,
|
||||
);
|
||||
|
||||
const lastUpdatedTimeAgoText =
|
||||
lastUpdatedTimeAgo > 0
|
||||
? ` ${lastUpdatedTimeAgo} ${t('knowledge.daysAgo')}`
|
||||
: t('knowledge.today');
|
||||
|
||||
return new ExternalKBCardVO({
|
||||
id: kb.uuid || '',
|
||||
name: kb.name,
|
||||
description: kb.description,
|
||||
apiUrl: kb.api_url,
|
||||
top_k: kb.top_k ?? 5,
|
||||
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
|
||||
});
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to load external knowledge bases:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const handleKBCardClick = (kbId: string) => {
|
||||
setSelectedKbId(kbId);
|
||||
setDetailDialogOpen(true);
|
||||
@@ -82,6 +144,77 @@ export default function KnowledgePage() {
|
||||
getKnowledgeBaseList();
|
||||
};
|
||||
|
||||
const handleExternalKBCardClick = (kbId: string) => {
|
||||
const kb = externalKBList.find((kb) => kb.id === kbId);
|
||||
if (kb) {
|
||||
// Load full data
|
||||
httpClient.getExternalKnowledgeBase(kbId).then((resp) => {
|
||||
setEditingExternalKB(resp.base);
|
||||
setExternalKBForm({
|
||||
name: resp.base.name,
|
||||
description: resp.base.description,
|
||||
api_url: resp.base.api_url,
|
||||
api_key: resp.base.api_key || '',
|
||||
top_k: resp.base.top_k,
|
||||
});
|
||||
setExternalKBDialogOpen(true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateExternalKB = () => {
|
||||
setEditingExternalKB(null);
|
||||
setExternalKBForm({
|
||||
name: '',
|
||||
description: '',
|
||||
api_url: '',
|
||||
api_key: '',
|
||||
top_k: 5,
|
||||
});
|
||||
setExternalKBDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveExternalKB = async () => {
|
||||
if (!externalKBForm.name || !externalKBForm.api_url) {
|
||||
toast.error(t('knowledge.externalApiUrlRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingExternalKB) {
|
||||
await httpClient.updateExternalKnowledgeBase(
|
||||
editingExternalKB.uuid!,
|
||||
externalKBForm as ExternalKnowledgeBase,
|
||||
);
|
||||
toast.success(t('knowledge.updateExternalSuccess'));
|
||||
} else {
|
||||
await httpClient.createExternalKnowledgeBase(
|
||||
externalKBForm as ExternalKnowledgeBase,
|
||||
);
|
||||
toast.success(t('knowledge.createExternalSuccess'));
|
||||
}
|
||||
setExternalKBDialogOpen(false);
|
||||
getExternalKBList();
|
||||
} catch (error) {
|
||||
toast.error('Failed to save external knowledge base');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteExternalKB = async () => {
|
||||
if (!editingExternalKB) return;
|
||||
|
||||
try {
|
||||
await httpClient.deleteExternalKnowledgeBase(editingExternalKB.uuid!);
|
||||
toast.success(t('knowledge.deleteExternalSuccess'));
|
||||
setExternalKBDialogOpen(false);
|
||||
getExternalKBList();
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete external knowledge base');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<KBDetailDialog
|
||||
@@ -94,22 +227,159 @@ export default function KnowledgePage() {
|
||||
onKbUpdated={handleKbUpdated}
|
||||
/>
|
||||
|
||||
<div className={styles.knowledgeListContainer}>
|
||||
<CreateCardComponent
|
||||
width={'100%'}
|
||||
height={'10rem'}
|
||||
plusSize={'90px'}
|
||||
onClick={handleCreateKBClick}
|
||||
/>
|
||||
|
||||
{knowledgeBaseList.map((kb) => {
|
||||
return (
|
||||
<div key={kb.id} onClick={() => handleKBCardClick(kb.id)}>
|
||||
<KBCard kbCardVO={kb} />
|
||||
<Dialog open={externalKBDialogOpen} onOpenChange={setExternalKBDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingExternalKB
|
||||
? t('knowledge.editKnowledgeBase')
|
||||
: t('knowledge.addExternal')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">
|
||||
{t('knowledge.kbName')}
|
||||
</label>
|
||||
<Input
|
||||
value={externalKBForm.name}
|
||||
onChange={(e) =>
|
||||
setExternalKBForm({ ...externalKBForm, name: e.target.value })
|
||||
}
|
||||
placeholder={t('knowledge.kbName')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium">
|
||||
{t('knowledge.kbDescription')}
|
||||
</label>
|
||||
<Textarea
|
||||
value={externalKBForm.description}
|
||||
onChange={(e) =>
|
||||
setExternalKBForm({
|
||||
...externalKBForm,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t('knowledge.kbDescription')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium">
|
||||
{t('knowledge.externalApiUrl')}
|
||||
</label>
|
||||
<Input
|
||||
value={externalKBForm.api_url}
|
||||
onChange={(e) =>
|
||||
setExternalKBForm({
|
||||
...externalKBForm,
|
||||
api_url: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t('knowledge.externalApiUrlPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium">
|
||||
{t('knowledge.externalApiKey')}
|
||||
</label>
|
||||
<Input
|
||||
value={externalKBForm.api_key}
|
||||
onChange={(e) =>
|
||||
setExternalKBForm({
|
||||
...externalKBForm,
|
||||
api_key: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t('knowledge.externalApiKeyPlaceholder')}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium">
|
||||
{t('knowledge.topK')}
|
||||
</label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={30}
|
||||
value={externalKBForm.top_k}
|
||||
onChange={(e) =>
|
||||
setExternalKBForm({
|
||||
...externalKBForm,
|
||||
top_k: parseInt(e.target.value) || 5,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
{editingExternalKB && (
|
||||
<Button variant="destructive" onClick={handleDeleteExternalKB}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={() => setExternalKBDialogOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSaveExternalKB}>{t('common.save')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<div className="flex flex-row justify-between items-center px-[0.8rem]">
|
||||
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
|
||||
<TabsTrigger value="builtin" className="px-6 py-4 cursor-pointer">
|
||||
{t('knowledge.builtIn')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="external" className="px-6 py-4 cursor-pointer">
|
||||
{t('knowledge.external')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="builtin">
|
||||
<div className={styles.knowledgeListContainer}>
|
||||
<CreateCardComponent
|
||||
width={'100%'}
|
||||
height={'10rem'}
|
||||
plusSize={'90px'}
|
||||
onClick={handleCreateKBClick}
|
||||
/>
|
||||
|
||||
{knowledgeBaseList.map((kb) => {
|
||||
return (
|
||||
<div key={kb.id} onClick={() => handleKBCardClick(kb.id)}>
|
||||
<KBCard kbCardVO={kb} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="external">
|
||||
<div className={styles.knowledgeListContainer}>
|
||||
<CreateCardComponent
|
||||
width={'100%'}
|
||||
height={'10rem'}
|
||||
plusSize={'90px'}
|
||||
onClick={handleCreateExternalKB}
|
||||
/>
|
||||
|
||||
{externalKBList.map((kb) => {
|
||||
return (
|
||||
<div
|
||||
key={kb.id}
|
||||
onClick={() => handleExternalKBCardClick(kb.id)}
|
||||
>
|
||||
<ExternalKBCard kbCardVO={kb} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,6 +162,24 @@ export interface KnowledgeBase {
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ExternalKnowledgeBase {
|
||||
uuid?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
api_url: string;
|
||||
api_key?: string;
|
||||
top_k: number;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface ApiRespExternalKnowledgeBases {
|
||||
bases: ExternalKnowledgeBase[];
|
||||
}
|
||||
|
||||
export interface ApiRespExternalKnowledgeBase {
|
||||
base: ExternalKnowledgeBase;
|
||||
}
|
||||
|
||||
export interface ApiRespKnowledgeBaseFiles {
|
||||
files: KnowledgeBaseFile[];
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ import {
|
||||
ApiRespMCPServers,
|
||||
ApiRespMCPServer,
|
||||
MCPServer,
|
||||
ExternalKnowledgeBase,
|
||||
ApiRespExternalKnowledgeBases,
|
||||
ApiRespExternalKnowledgeBase,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { Plugin } from '@/app/infra/entities/plugin';
|
||||
import { GetBotLogsRequest } from '@/app/infra/http/requestParam/bots/GetBotLogsRequest';
|
||||
@@ -439,6 +442,43 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.post(`/api/v1/knowledge/bases/${uuid}/retrieve`, { query });
|
||||
}
|
||||
|
||||
// ============ External Knowledge Base API ============
|
||||
public getExternalKnowledgeBases(): Promise<ApiRespExternalKnowledgeBases> {
|
||||
return this.get('/api/v1/knowledge/external-bases');
|
||||
}
|
||||
|
||||
public getExternalKnowledgeBase(
|
||||
uuid: string,
|
||||
): Promise<ApiRespExternalKnowledgeBase> {
|
||||
return this.get(`/api/v1/knowledge/external-bases/${uuid}`);
|
||||
}
|
||||
|
||||
public createExternalKnowledgeBase(
|
||||
base: ExternalKnowledgeBase,
|
||||
): Promise<{ uuid: string }> {
|
||||
return this.post('/api/v1/knowledge/external-bases', base);
|
||||
}
|
||||
|
||||
public updateExternalKnowledgeBase(
|
||||
uuid: string,
|
||||
base: ExternalKnowledgeBase,
|
||||
): Promise<{ uuid: string }> {
|
||||
return this.put(`/api/v1/knowledge/external-bases/${uuid}`, base);
|
||||
}
|
||||
|
||||
public deleteExternalKnowledgeBase(uuid: string): Promise<object> {
|
||||
return this.delete(`/api/v1/knowledge/external-bases/${uuid}`);
|
||||
}
|
||||
|
||||
public retrieveExternalKnowledgeBase(
|
||||
uuid: string,
|
||||
query: string,
|
||||
): Promise<ApiRespKnowledgeBaseRetrieve> {
|
||||
return this.post(`/api/v1/knowledge/external-bases/${uuid}/retrieve`, {
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
// ============ Plugins API ============
|
||||
public getPlugins(): Promise<ApiRespPlugins> {
|
||||
return this.get('/api/v1/plugins');
|
||||
|
||||
@@ -571,6 +571,19 @@ const enUS = {
|
||||
fileName: 'File Name',
|
||||
noResults: 'No results',
|
||||
retrieveError: 'Retrieve failed',
|
||||
builtIn: 'Built-in',
|
||||
external: 'External',
|
||||
addExternal: 'Add External Knowledge Base',
|
||||
externalApiUrl: 'API URL',
|
||||
externalApiUrlPlaceholder: 'Enter external knowledge base API URL',
|
||||
externalApiUrlRequired: 'API URL cannot be empty',
|
||||
externalApiKey: 'API Key (Optional)',
|
||||
externalApiKeyPlaceholder: 'Enter API key if required',
|
||||
externalKbDescription:
|
||||
'External knowledge bases retrieve documents via HTTP API',
|
||||
createExternalSuccess: 'External knowledge base created successfully',
|
||||
updateExternalSuccess: 'External knowledge base updated successfully',
|
||||
deleteExternalSuccess: 'External knowledge base deleted successfully',
|
||||
},
|
||||
register: {
|
||||
title: 'Initialize LangBot 👋',
|
||||
|
||||
@@ -575,6 +575,18 @@ const jaJP = {
|
||||
fileName: 'ファイル名',
|
||||
noResults: '検索結果がありません',
|
||||
retrieveError: '検索に失敗しました',
|
||||
builtIn: '内蔵',
|
||||
external: '外部ナレッジベース',
|
||||
addExternal: '外部ナレッジベースを追加',
|
||||
externalApiUrl: 'API URL',
|
||||
externalApiUrlPlaceholder: '外部ナレッジベースのAPI URLを入力',
|
||||
externalApiUrlRequired: 'API URLは空にできません',
|
||||
externalApiKey: 'API キー(オプション)',
|
||||
externalApiKeyPlaceholder: '必要に応じてAPIキーを入力してください',
|
||||
externalKbDescription: '外部ナレッジベースはHTTP APIを介してドキュメントを取得します',
|
||||
createExternalSuccess: '外部ナレッジベースが正常に作成されました',
|
||||
updateExternalSuccess: '外部ナレッジベースが正常に更新されました',
|
||||
deleteExternalSuccess: '外部ナレッジベースが正常に削除されました',
|
||||
},
|
||||
register: {
|
||||
title: 'LangBot を初期化 👋',
|
||||
|
||||
@@ -548,6 +548,18 @@ const zhHans = {
|
||||
fileName: '文件名',
|
||||
noResults: '暂无结果',
|
||||
retrieveError: '检索失败',
|
||||
builtIn: '内置',
|
||||
external: '外部知识库',
|
||||
addExternal: '添加外部知识库',
|
||||
externalApiUrl: 'API 地址',
|
||||
externalApiUrlPlaceholder: '输入外部知识库 API 地址',
|
||||
externalApiUrlRequired: 'API 地址不能为空',
|
||||
externalApiKey: 'API 密钥(可选)',
|
||||
externalApiKeyPlaceholder: '如需要请输入 API 密钥',
|
||||
externalKbDescription: '外部知识库通过 HTTP API 检索文档',
|
||||
createExternalSuccess: '外部知识库创建成功',
|
||||
updateExternalSuccess: '外部知识库更新成功',
|
||||
deleteExternalSuccess: '外部知识库删除成功',
|
||||
},
|
||||
register: {
|
||||
title: '初始化 LangBot 👋',
|
||||
|
||||
@@ -545,6 +545,18 @@ const zhHant = {
|
||||
fileName: '文檔名稱',
|
||||
noResults: '暫無結果',
|
||||
retrieveError: '檢索失敗',
|
||||
builtIn: '內置',
|
||||
external: '外部知識庫',
|
||||
addExternal: '添加外部知識庫',
|
||||
externalApiUrl: 'API 地址',
|
||||
externalApiUrlPlaceholder: '輸入外部知識庫 API 地址',
|
||||
externalApiUrlRequired: 'API 地址不能為空',
|
||||
externalApiKey: 'API 密鑰(可選)',
|
||||
externalApiKeyPlaceholder: '如需要請輸入 API 密鑰',
|
||||
externalKbDescription: '外部知識庫通過 HTTP API 檢索文檔',
|
||||
createExternalSuccess: '外部知識庫創建成功',
|
||||
updateExternalSuccess: '外部知識庫更新成功',
|
||||
deleteExternalSuccess: '外部知識庫刪除成功',
|
||||
},
|
||||
register: {
|
||||
title: '初始化 LangBot 👋',
|
||||
|
||||
Reference in New Issue
Block a user