Spaces:
Paused
Paused
| # -*- coding: utf-8 -*- | |
| """Knowledge base router — manage knowledge bases and their documents. | |
| A knowledge base is the user-facing concept; physically each one maps | |
| to a single vector store collection (in the MVP isolation strategy). | |
| The HTTP layer is intentionally thin — every endpoint translates the | |
| request into a single :class:`~agentscope.app._service. | |
| KnowledgeBaseService` call and returns the result. | |
| """ | |
| from fastapi import ( | |
| APIRouter, | |
| Depends, | |
| File, | |
| Form, | |
| Path, | |
| Query, | |
| UploadFile, | |
| status, | |
| ) | |
| from ..deps import ( | |
| get_current_user_id, | |
| get_knowledge_base_manager, | |
| get_knowledge_base_service, | |
| get_knowledge_parsers, | |
| get_storage, | |
| ) | |
| from ._schema import ( | |
| CreateKnowledgeBaseRequest, | |
| CreateKnowledgeBaseResponse, | |
| KbEmbeddingProvider, | |
| KbMiddlewareParametersSchemaResponse, | |
| KnowledgeBaseView, | |
| KnowledgeDocumentView, | |
| ListKbEmbeddingModelsResponse, | |
| ListKnowledgeBasesResponse, | |
| ListKnowledgeDocumentsResponse, | |
| ListKnowledgeDocumentStatusResponse, | |
| ListSupportedContentTypesResponse, | |
| SearchKnowledgeBaseRequest, | |
| SearchKnowledgeBaseResponse, | |
| UpdateKnowledgeBaseRequest, | |
| UploadKnowledgeDocumentResponse, | |
| ) | |
| from ...credential import CredentialFactory | |
| from ..rag.knowledge_base_manager import KnowledgeBaseManagerBase | |
| from ..storage import StorageBase | |
| from .._service import KnowledgeBaseService | |
| from ...middleware import RAGMiddleware | |
| from ...rag import ParserBase | |
| knowledge_base_router = APIRouter( | |
| prefix="/knowledge_bases", | |
| tags=["knowledge_bases"], | |
| responses={404: {"description": "Not found"}}, | |
| ) | |
| async def list_kb_embedding_models( | |
| user_id: str = Depends(get_current_user_id), | |
| storage: "StorageBase" = Depends(get_storage), | |
| manager: "KnowledgeBaseManagerBase" = Depends( | |
| get_knowledge_base_manager, | |
| ), | |
| ) -> ListKbEmbeddingModelsResponse: | |
| """List embedding models the user can pick at KB-creation time. | |
| Walks the caller's credentials, looks up each provider's | |
| embedding model class, gathers its model cards, and projects | |
| each card through the manager's :class:`DimensionPolicy`. | |
| Incompatible cards are dropped; matryoshka cards under a | |
| ``FIXED`` / ``LOCKED_BY_EXISTING`` policy are narrowed to the | |
| locked dimension. Providers that end up with zero compatible | |
| models are omitted from the response entirely. | |
| Args: | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| storage (`StorageBase`): | |
| Injected storage backend used to enumerate credentials. | |
| manager (`KnowledgeBaseManagerBase`): | |
| Injected knowledge base manager. | |
| Returns: | |
| `ListKbEmbeddingModelsResponse`: | |
| One entry per credential with at least one compatible | |
| embedding model, plus the policy used for filtering. | |
| """ | |
| policy = await manager.get_dimension_policy() | |
| credentials = await storage.list_credentials(user_id) | |
| providers: list[KbEmbeddingProvider] = [] | |
| for credential in credentials: | |
| credential_type = credential.data.get("type") | |
| if not credential_type: | |
| continue | |
| credential_cls = CredentialFactory.get_credential_class( | |
| credential_type, | |
| ) | |
| if credential_cls is None: | |
| continue | |
| embedding_cls = credential_cls.get_embedding_model_class() | |
| if embedding_cls is None: | |
| continue | |
| filtered = [] | |
| for card in embedding_cls.list_models(): | |
| projected = policy.filter_card(card) | |
| if projected is not None: | |
| filtered.append(projected) | |
| if not filtered: | |
| continue | |
| providers.append( | |
| KbEmbeddingProvider(credential=credential, models=filtered), | |
| ) | |
| return ListKbEmbeddingModelsResponse(providers=providers, policy=policy) | |
| async def get_kb_middleware_parameters_schema( | |
| _: str = Depends(get_current_user_id), | |
| ) -> KbMiddlewareParametersSchemaResponse: | |
| """Return the parameter schema for | |
| :class:`agentscope.middleware.RAGMiddleware`. | |
| The schema is shaped like every other ``parameter_schema`` served | |
| by this service — title / description / default / enum / minimum | |
| / maximum — so the front-end can render the session-level KB | |
| attachment form with the same schema-driven component used for | |
| model parameters. | |
| Args: | |
| _ (`str`): | |
| Injected authenticated user ID; only used to gate the | |
| endpoint behind authentication. | |
| Returns: | |
| `KbMiddlewareParametersSchemaResponse`: | |
| The JSON Schema describing the middleware's | |
| user-tunable parameters. | |
| """ | |
| return KbMiddlewareParametersSchemaResponse( | |
| parameter_schema=(RAGMiddleware.Parameters.model_json_schema()), | |
| ) | |
| async def list_supported_content_types( | |
| _: str = Depends(get_current_user_id), | |
| parsers: list[ParserBase] | |
| | dict[str, ParserBase] = Depends( | |
| get_knowledge_parsers, | |
| ), | |
| ) -> ListSupportedContentTypesResponse: | |
| """Advertise the union of media types and filename extensions every | |
| registered parser accepts. | |
| Used by the front-end to populate the document picker's ``accept`` | |
| attribute and to reject drag-dropped files whose extension lies | |
| outside the supported set before the upload starts. Routing on | |
| upload still goes through the media type — this endpoint is a | |
| capability hint, not authoritative dispatch. | |
| Args: | |
| _ (`str`): | |
| Injected authenticated user ID; only used to gate the | |
| endpoint behind authentication. | |
| parsers (`list[ParserBase] | dict[str, ParserBase]`): | |
| Injected parser registry — the same value the index worker | |
| uses to dispatch uploads. | |
| Returns: | |
| `ListSupportedContentTypesResponse`: | |
| Deduplicated, sorted unions of ``media_types`` and | |
| ``extensions``. | |
| """ | |
| parser_iter = parsers.values() if isinstance(parsers, dict) else parsers | |
| media_types: set[str] = set() | |
| extensions: set[str] = set() | |
| for parser in parser_iter: | |
| media_types.update(parser.supported_media_types) | |
| extensions.update(parser.supported_extensions()) | |
| return ListSupportedContentTypesResponse( | |
| media_types=sorted(media_types), | |
| extensions=sorted(extensions), | |
| ) | |
| # ---------------------------------------------------------------------- | |
| # Knowledge base management | |
| # ---------------------------------------------------------------------- | |
| async def create_knowledge_base( | |
| body: CreateKnowledgeBaseRequest, | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> CreateKnowledgeBaseResponse: | |
| """Create a new knowledge base for the authenticated user. | |
| Allocates a fresh vector store collection sized to the embedding | |
| model's output dimension and persists the knowledge base record. | |
| Args: | |
| body (`CreateKnowledgeBaseRequest`): | |
| Knowledge base name, description, and embedding model | |
| configuration. | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| Returns: | |
| `CreateKnowledgeBaseResponse`: | |
| The server-assigned knowledge base identifier. | |
| """ | |
| record = await service.create_knowledge_base( | |
| user_id=user_id, | |
| name=body.name, | |
| description=body.description, | |
| embedding_model_config=body.embedding_model_config, | |
| ) | |
| return CreateKnowledgeBaseResponse(knowledge_base_id=record.id) | |
| async def list_knowledge_bases( | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> ListKnowledgeBasesResponse: | |
| """Return all knowledge bases owned by the authenticated user. | |
| Args: | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| Returns: | |
| `ListKnowledgeBasesResponse`: | |
| The user's knowledge bases. | |
| """ | |
| records = await service.list_knowledge_bases(user_id) | |
| views = [ | |
| KnowledgeBaseView( | |
| id=record.id, | |
| name=record.name, | |
| description=record.description, | |
| embedding_model_config=record.embedding_model_config, | |
| created_at=record.created_at, | |
| updated_at=record.updated_at, | |
| ) | |
| for record in records | |
| ] | |
| return ListKnowledgeBasesResponse(knowledge_bases=views, total=len(views)) | |
| async def update_knowledge_base( | |
| body: UpdateKnowledgeBaseRequest, | |
| knowledge_base_id: str = Path(description="The knowledge base id."), | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> KnowledgeBaseView: | |
| """Update mutable fields on a knowledge base. | |
| Only ``name`` and ``description`` can be updated. The embedding | |
| model configuration is pinned at creation time and cannot be | |
| changed. | |
| Args: | |
| body (`UpdateKnowledgeBaseRequest`): | |
| The fields to update; omitted fields stay unchanged. | |
| knowledge_base_id (`str`): | |
| The knowledge base to update. | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| Returns: | |
| `KnowledgeBaseView`: | |
| The knowledge base record after the update. | |
| """ | |
| record = await service.update_knowledge_base( | |
| user_id=user_id, | |
| knowledge_base_id=knowledge_base_id, | |
| name=body.name, | |
| description=body.description, | |
| ) | |
| return KnowledgeBaseView( | |
| id=record.id, | |
| name=record.name, | |
| description=record.description, | |
| embedding_model_config=record.embedding_model_config, | |
| created_at=record.created_at, | |
| updated_at=record.updated_at, | |
| ) | |
| async def delete_knowledge_base( | |
| knowledge_base_id: str = Path(description="The knowledge base id."), | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> None: | |
| """Permanently delete a knowledge base. | |
| Drops the underlying vector store collection together with every | |
| associated document and the knowledge base record itself. | |
| Args: | |
| knowledge_base_id (`str`): | |
| The knowledge base to delete. | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| """ | |
| await service.delete_knowledge_base(user_id, knowledge_base_id) | |
| # ---------------------------------------------------------------------- | |
| # Document management | |
| # ---------------------------------------------------------------------- | |
| async def list_knowledge_documents( | |
| knowledge_base_id: str = Path(description="The knowledge base id."), | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> ListKnowledgeDocumentsResponse: | |
| """List every document registered against a knowledge base. | |
| Reads from the storage backend (service-mode source of truth), so | |
| documents in any lifecycle state — including ``pending`` / | |
| ``parsing`` / ``error`` — are returned alongside ``ready`` ones. | |
| Args: | |
| knowledge_base_id (`str`): | |
| The target knowledge base id. | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| Returns: | |
| `ListKnowledgeDocumentsResponse`: | |
| One view per registered document. | |
| """ | |
| records = await service.list_documents(user_id, knowledge_base_id) | |
| views = [KnowledgeDocumentView.from_record(r) for r in records] | |
| return ListKnowledgeDocumentsResponse( | |
| documents=views, | |
| total=len(views), | |
| ) | |
| async def list_knowledge_document_status( | |
| knowledge_base_id: str = Path(description="The knowledge base id."), | |
| ids: str = Query( | |
| description=( | |
| "Comma-separated list of document ids to query. " | |
| "Missing ids are silently omitted from the response." | |
| ), | |
| ), | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> ListKnowledgeDocumentStatusResponse: | |
| """Return the current lifecycle state of a batch of documents. | |
| Designed for the front-end's status polling loop: the page sends | |
| every in-flight document id at once so per-document round-trips | |
| do not multiply with concurrency. | |
| Args: | |
| knowledge_base_id (`str`): | |
| The target knowledge base id. | |
| ids (`str`): | |
| Comma-separated document ids. | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| Returns: | |
| `ListKnowledgeDocumentStatusResponse`: | |
| Views for the matched documents. | |
| """ | |
| document_ids = [tok for tok in (s.strip() for s in ids.split(",")) if tok] | |
| records = await service.get_document_status( | |
| user_id, | |
| knowledge_base_id, | |
| document_ids, | |
| ) | |
| return ListKnowledgeDocumentStatusResponse( | |
| items=[KnowledgeDocumentView.from_record(r) for r in records], | |
| ) | |
| async def upload_knowledge_document( | |
| knowledge_base_id: str = Path(description="The knowledge base id."), | |
| file: UploadFile = File( | |
| description="The document to index (PDF, TXT, Markdown, …).", | |
| ), | |
| content_type: str | |
| | None = Form( | |
| default=None, | |
| description=( | |
| "Override the IANA media type used to route the upload. " | |
| "Defaults to the type guessed from the filename." | |
| ), | |
| ), | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> UploadKnowledgeDocumentResponse: | |
| """Register an uploaded document and dispatch it for indexing. | |
| The HTTP connection covers only the upload phase: the request body | |
| is streamed into the blob store, a ``pending`` document record is | |
| persisted, the indexing task is dispatched, and the response is | |
| returned. Parsing / chunking / embedding happen asynchronously in | |
| a worker; the client tracks progress via | |
| :func:`list_knowledge_document_status`. | |
| Args: | |
| knowledge_base_id (`str`): | |
| The knowledge base to receive the document. | |
| file (`UploadFile`): | |
| The uploaded file (multipart/form-data). | |
| content_type (`str | None`, optional): | |
| Override the IANA media type used to route the upload. | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| Returns: | |
| `UploadKnowledgeDocumentResponse`: | |
| The server-assigned document id, filename, and the | |
| initial lifecycle state (always ``"pending"``). | |
| """ | |
| record = await service.register_document( | |
| user_id=user_id, | |
| knowledge_base_id=knowledge_base_id, | |
| filename=file.filename or "uploaded_file", | |
| stream=file.file, | |
| size=file.size or 0, | |
| content_type=content_type or file.content_type, | |
| ) | |
| return UploadKnowledgeDocumentResponse( | |
| document_id=record.id, | |
| filename=record.data.filename, | |
| status=record.data.status, | |
| ) | |
| async def delete_knowledge_document( | |
| knowledge_base_id: str = Path(description="The knowledge base id."), | |
| document_id: str = Path(description="The document id."), | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> None: | |
| """Remove a document and all its chunks from a knowledge base. | |
| Args: | |
| knowledge_base_id (`str`): | |
| The knowledge base the document belongs to. | |
| document_id (`str`): | |
| The document to delete. | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| """ | |
| await service.delete_document(user_id, knowledge_base_id, document_id) | |
| # ---------------------------------------------------------------------- | |
| # Search | |
| # ---------------------------------------------------------------------- | |
| async def search_knowledge_base( | |
| body: SearchKnowledgeBaseRequest, | |
| knowledge_base_id: str = Path(description="The knowledge base id."), | |
| user_id: str = Depends(get_current_user_id), | |
| service: "KnowledgeBaseService" = Depends(get_knowledge_base_service), | |
| ) -> SearchKnowledgeBaseResponse: | |
| """Run a similarity search over a knowledge base. | |
| Embeds the query with the knowledge base's configured embedding | |
| model and returns the top-K most similar chunks. | |
| Args: | |
| body (`SearchKnowledgeBaseRequest`): | |
| The query text and ``top_k``. | |
| knowledge_base_id (`str`): | |
| The knowledge base to search. | |
| user_id (`str`): | |
| Injected authenticated user ID. | |
| service (`KnowledgeBaseService`): | |
| Injected knowledge base service. | |
| Returns: | |
| `SearchKnowledgeBaseResponse`: | |
| Matched chunks ordered by descending similarity. | |
| """ | |
| results = await service.search( | |
| user_id=user_id, | |
| knowledge_base_id=knowledge_base_id, | |
| query=body.query, | |
| top_k=body.top_k, | |
| ) | |
| return SearchKnowledgeBaseResponse(results=results, total=len(results)) | |