diff --git a/src/langbot/pkg/api/http/controller/groups/knowledge/__init__.py b/src/langbot/pkg/api/http/controller/groups/knowledge/__init__.py index e69de29bb..3d892aebd 100644 --- a/src/langbot/pkg/api/http/controller/groups/knowledge/__init__.py +++ b/src/langbot/pkg/api/http/controller/groups/knowledge/__init__.py @@ -0,0 +1 @@ +from . import base, external diff --git a/src/langbot/pkg/api/http/controller/groups/knowledge/external.py b/src/langbot/pkg/api/http/controller/groups/knowledge/external.py new file mode 100644 index 000000000..dcdeb769b --- /dev/null +++ b/src/langbot/pkg/api/http/controller/groups/knowledge/external.py @@ -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( + '/', + 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( + '//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}) diff --git a/src/langbot/pkg/api/http/service/knowledge.py b/src/langbot/pkg/api/http/service/knowledge.py index 7b748bc6b..65c350fbd 100644 --- a/src/langbot/pkg/api/http/service/knowledge.py +++ b/src/langbot/pkg/api/http/service/knowledge.py @@ -71,6 +71,9 @@ async def store_file(self, kb_uuid: str, file_id: str) -> int: 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 @@ async def retrieve_knowledge_base(self, kb_uuid: str, query: str) -> list[dict]: 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 @@ async def delete_file(self, kb_uuid: str, file_id: str) -> None: 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 @@ async def delete_knowledge_base(self, kb_uuid: str) -> None: 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 + ) + ) diff --git a/src/langbot/pkg/entity/persistence/rag.py b/src/langbot/pkg/entity/persistence/rag.py index 0ff93d283..b7ef83d4d 100644 --- a/src/langbot/pkg/entity/persistence/rag.py +++ b/src/langbot/pkg/entity/persistence/rag.py @@ -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) diff --git a/src/langbot/pkg/provider/runners/localagent.py b/src/langbot/pkg/provider/runners/localagent.py index 6375ca315..983c7cf19 100644 --- a/src/langbot/pkg/provider/runners/localagent.py +++ b/src/langbot/pkg/provider/runners/localagent.py @@ -73,7 +73,15 @@ async def run( 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) diff --git a/src/langbot/pkg/rag/knowledge/base.py b/src/langbot/pkg/rag/knowledge/base.py new file mode 100644 index 000000000..7cd8d8e78 --- /dev/null +++ b/src/langbot/pkg/rag/knowledge/base.py @@ -0,0 +1,55 @@ +"""Base classes and interfaces for knowledge bases""" +from __future__ import annotations + +import abc +import typing + +from langbot.pkg.core import app +from langbot.pkg.entity.rag import retriever as retriever_entities + + +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[retriever_entities.RetrieveResultEntry]: + """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 diff --git a/src/langbot/pkg/rag/knowledge/external.py b/src/langbot/pkg/rag/knowledge/external.py new file mode 100644 index 000000000..e74db326e --- /dev/null +++ b/src/langbot/pkg/rag/knowledge/external.py @@ -0,0 +1,137 @@ +"""External knowledge base implementation""" +from __future__ import annotations + +import aiohttp +import typing + +from langbot.pkg.core import app +from langbot.pkg.entity.persistence import rag as persistence_rag +from langbot.pkg.entity.rag import retriever as retriever_entities +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[retriever_entities.RetrieveResultEntry]: + """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( + retriever_entities.RetrieveResultEntry( + 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 diff --git a/src/langbot/pkg/rag/knowledge/kbmgr.py b/src/langbot/pkg/rag/knowledge/kbmgr.py index 17e2af324..83e2aaf0a 100644 --- a/src/langbot/pkg/rag/knowledge/kbmgr.py +++ b/src/langbot/pkg/rag/knowledge/kbmgr.py @@ -11,9 +11,11 @@ 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 .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) @@ -206,6 +208,18 @@ async def delete_file(self, file_id: str): 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 @@ async def dispose(self): 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 @@ async def load_knowledge_bases_from_db(self): 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 @@ async def load_knowledge_bases_from_db(self): 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 @@ async def load_knowledge_base( 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 diff --git a/web/src/app/home/knowledge/components/external-kb-card/ExternalKBCard.tsx b/web/src/app/home/knowledge/components/external-kb-card/ExternalKBCard.tsx new file mode 100644 index 000000000..ffe0b4676 --- /dev/null +++ b/web/src/app/home/knowledge/components/external-kb-card/ExternalKBCard.tsx @@ -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 ( +
+
+
+
+ {kbCardVO.name} +
+
+ {kbCardVO.description} +
+
+ +
+ + + +
+ {t('knowledge.updateTime')} + {kbCardVO.lastUpdatedTimeAgo} +
+
+
+
+ ); +} diff --git a/web/src/app/home/knowledge/components/external-kb-card/ExternalKBCardVO.ts b/web/src/app/home/knowledge/components/external-kb-card/ExternalKBCardVO.ts new file mode 100644 index 000000000..c6a357013 --- /dev/null +++ b/web/src/app/home/knowledge/components/external-kb-card/ExternalKBCardVO.ts @@ -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; + } +} diff --git a/web/src/app/home/knowledge/knowledgeBase.module.css b/web/src/app/home/knowledge/knowledgeBase.module.css index e811b521e..8305fc13a 100644 --- a/web/src/app/home/knowledge/knowledgeBase.module.css +++ b/web/src/app/home/knowledge/knowledgeBase.module.css @@ -5,6 +5,7 @@ .knowledgeListContainer { width: 100%; + margin-top: 2rem; padding-left: 0.8rem; padding-right: 0.8rem; display: grid; diff --git a/web/src/app/home/knowledge/page.tsx b/web/src/app/home/knowledge/page.tsx index 85e99c56b..cf0eb792e 100644 --- a/web/src/app/home/knowledge/page.tsx +++ b/web/src/app/home/knowledge/page.tsx @@ -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( [], ); + const [externalKBList, setExternalKBList] = useState([]); const [selectedKbId, setSelectedKbId] = useState(''); const [detailDialogOpen, setDetailDialogOpen] = useState(false); + const [externalKBDialogOpen, setExternalKBDialogOpen] = useState(false); + const [editingExternalKB, setEditingExternalKB] = + useState(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 (
-
- - - {knowledgeBaseList.map((kb) => { - return ( -
handleKBCardClick(kb.id)}> - + + + + + {editingExternalKB + ? t('knowledge.editKnowledgeBase') + : t('knowledge.addExternal')} + + +
+
+ + + setExternalKBForm({ ...externalKBForm, name: e.target.value }) + } + placeholder={t('knowledge.kbName')} + />
- ); - })} -
+
+ +