-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix: align knowledge base CRUD API contract with kb_name, canonical payload fields, and matching OpenAPI types #9000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -205,14 +205,42 @@ class ImMessageRequest(OpenModel): | |||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class KnowledgeBaseRequest(OpenModel): | ||||||||||||||||||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||||||||||||||||||
| kb_id: str | None = None | ||||||||||||||||||
| name: str | None = None | ||||||||||||||||||
| kb_name: str | None = None | ||||||||||||||||||
| description: str | None = None | ||||||||||||||||||
| emoji: str | None = None | ||||||||||||||||||
| embedding_provider_id: str | None = None | ||||||||||||||||||
| rerank_provider_id: str | None = None | ||||||||||||||||||
| chunk_size: int | None = None | ||||||||||||||||||
| chunk_overlap: int | None = None | ||||||||||||||||||
|
|
||||||||||||||||||
| top_k_dense: int | None = None | ||||||||||||||||||
| top_k_sparse: int | None = None | ||||||||||||||||||
| top_m_final: int | None = None | ||||||||||||||||||
|
|
||||||||||||||||||
| def canonical_payload(self) -> dict[str, Any]: | ||||||||||||||||||
| """Return the service-facing knowledge base payload. | ||||||||||||||||||
|
|
||||||||||||||||||
| Returns: | ||||||||||||||||||
| Dictionary accepted by KnowledgeBaseService. | ||||||||||||||||||
| """ | ||||||||||||||||||
| data = self.model_dump( | ||||||||||||||||||
| exclude_unset=True, | ||||||||||||||||||
| include={ | ||||||||||||||||||
| "kb_name", | ||||||||||||||||||
| "description", | ||||||||||||||||||
| "emoji", | ||||||||||||||||||
| "embedding_provider_id", | ||||||||||||||||||
| "rerank_provider_id", | ||||||||||||||||||
| "chunk_size", | ||||||||||||||||||
| "chunk_overlap", | ||||||||||||||||||
| "top_k_dense", | ||||||||||||||||||
| "top_k_sparse", | ||||||||||||||||||
| "top_m_final", | ||||||||||||||||||
| }, | ||||||||||||||||||
| ) | ||||||||||||||||||
| legacy_name = getattr(self, "name", None) | ||||||||||||||||||
| if data.get("kb_name") is None and legacy_name is not None: | ||||||||||||||||||
| data["kb_name"] = legacy_name | ||||||||||||||||||
| return data | ||||||||||||||||||
|
|
||||||||||||||||||
| class KnowledgeBaseImportRequest(OpenModel): | ||||||||||||||||||
|
Comment on lines
249
to
250
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR description mentions syncing Please define
Suggested change
|
||||||||||||||||||
| documents: list[dict[str, Any]] | None = None | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -29,6 +29,22 @@ def __init__(self, core_lifecycle: AstrBotCoreLifecycle) -> None: | |||||
| def _payload(data: object) -> dict[str, Any]: | ||||||
| return data if isinstance(data, dict) else {} | ||||||
|
|
||||||
| @staticmethod | ||||||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||||||
| def _canonical_kb_payload(data: object) -> dict[str, Any]: | ||||||
| """Normalize knowledge base create/update payloads. | ||||||
|
|
||||||
| Args: | ||||||
| data: Request payload from v1 or legacy Dashboard routes. | ||||||
|
|
||||||
| Returns: | ||||||
| Payload using the service's canonical field names. | ||||||
| """ | ||||||
| payload = KnowledgeBaseService._payload(data).copy() | ||||||
| if payload.get("kb_name") is None and payload.get("name") is not None: | ||||||
| payload["kb_name"] = payload["name"] | ||||||
| payload.pop("name", None) | ||||||
| return payload | ||||||
|
|
||||||
| def get_kb_manager(self): | ||||||
| return self.core_lifecycle.kb_manager | ||||||
|
|
||||||
|
|
@@ -263,19 +279,30 @@ async def background_import_task( | |||||
| logger.error(traceback.format_exc()) | ||||||
| self.set_task_result(task_id, "failed", error=str(exc)) | ||||||
|
|
||||||
| async def list_kbs(self, *, page: int, page_size: int) -> dict[str, Any]: | ||||||
| async def list_kbs(self, *, page: int, page_size: int | None) -> dict[str, Any]: | ||||||
| kb_manager = self.get_kb_manager() | ||||||
| kbs = await kb_manager.list_kbs() | ||||||
|
|
||||||
| kb_list = [] | ||||||
| for kb in kbs: | ||||||
| selected_kbs = kbs | ||||||
| if page_size is not None: | ||||||
| start = max(page - 1, 0) * page_size | ||||||
| end = start + page_size | ||||||
| selected_kbs = kbs[start:end] | ||||||
|
|
||||||
| for kb in selected_kbs: | ||||||
| kb_dict = kb.model_dump() | ||||||
| kb_helper = await kb_manager.get_kb(kb.kb_id) | ||||||
| if kb_helper and kb_helper.init_error: | ||||||
| kb_dict["init_error"] = kb_helper.init_error | ||||||
| kb_list.append(kb_dict) | ||||||
|
|
||||||
| return {"items": kb_list, "page": page, "page_size": page_size} | ||||||
| return { | ||||||
| "items": kb_list, | ||||||
| "page": page, | ||||||
| "page_size": page_size if page_size is not None else len(kbs), | ||||||
| "total": len(kbs), | ||||||
| } | ||||||
|
|
||||||
| async def list_kbs_from_dashboard_query(self, *, page, page_size) -> dict[str, Any]: | ||||||
| return await self.list_kbs( | ||||||
|
|
@@ -285,7 +312,7 @@ async def list_kbs_from_dashboard_query(self, *, page, page_size) -> dict[str, A | |||||
|
|
||||||
| async def create_kb(self, data: object) -> tuple[dict[str, Any], str]: | ||||||
| kb_manager = self.get_kb_manager() | ||||||
| payload = self._payload(data) | ||||||
| payload = self._canonical_kb_payload(data) | ||||||
| kb_name = payload.get("kb_name") | ||||||
| if not kb_name: | ||||||
| raise KnowledgeBaseServiceError("知识库名称不能为空") | ||||||
|
|
@@ -355,7 +382,7 @@ async def get_kb_from_dashboard_query(self, kb_id: str | None) -> dict[str, Any] | |||||
| return await self.get_kb(kb_id) | ||||||
|
|
||||||
| async def update_kb(self, data: object) -> tuple[dict[str, Any], str]: | ||||||
| payload = self._payload(data) | ||||||
| payload = self._canonical_kb_payload(data) | ||||||
| kb_id = payload.get("kb_id") | ||||||
| if not kb_id: | ||||||
| raise KnowledgeBaseServiceError("缺少参数 kb_id") | ||||||
|
|
@@ -372,28 +399,20 @@ async def update_kb(self, data: object) -> tuple[dict[str, Any], str]: | |||||
| "top_k_sparse", | ||||||
| "top_m_final", | ||||||
| ] | ||||||
| if all(payload.get(key) is None for key in update_keys): | ||||||
| provided_updates = {key: payload[key] for key in update_keys if key in payload} | ||||||
| if not provided_updates: | ||||||
| raise KnowledgeBaseServiceError("至少需要提供一个更新字段") | ||||||
|
|
||||||
| current_kb = await self.get_kb_manager().get_kb(kb_id) | ||||||
| kb_name = payload.get("kb_name") | ||||||
| if kb_name is None: | ||||||
| if not current_kb: | ||||||
| raise KnowledgeBaseServiceError("知识库不存在") | ||||||
| kb_name = current_kb.kb.kb_name | ||||||
| if not current_kb: | ||||||
| raise KnowledgeBaseServiceError("知识库不存在") | ||||||
| current = current_kb.kb | ||||||
| update_data = {key: getattr(current, key) for key in update_keys} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Using
Suggested change
|
||||||
| update_data.update(provided_updates) | ||||||
|
|
||||||
| kb_helper = await self.get_kb_manager().update_kb( | ||||||
| kb_id=kb_id, | ||||||
| kb_name=kb_name, | ||||||
| description=payload.get("description"), | ||||||
| emoji=payload.get("emoji"), | ||||||
| embedding_provider_id=payload.get("embedding_provider_id"), | ||||||
| rerank_provider_id=payload.get("rerank_provider_id"), | ||||||
| chunk_size=payload.get("chunk_size"), | ||||||
| chunk_overlap=payload.get("chunk_overlap"), | ||||||
| top_k_dense=payload.get("top_k_dense"), | ||||||
| top_k_sparse=payload.get("top_k_sparse"), | ||||||
| top_m_final=payload.get("top_m_final"), | ||||||
| **update_data, | ||||||
| ) | ||||||
| if not kb_helper: | ||||||
| raise KnowledgeBaseServiceError("知识库不存在") | ||||||
|
|
@@ -738,11 +757,11 @@ async def retrieve(self, data: object) -> dict[str, Any]: | |||||
|
|
||||||
| if not query: | ||||||
| raise KnowledgeBaseServiceError("缺少参数 query") | ||||||
| kb_manager = self.get_kb_manager() | ||||||
| if not kb_names or not isinstance(kb_names, list): | ||||||
| raise KnowledgeBaseServiceError("缺少参数 kb_names 或格式错误") | ||||||
|
|
||||||
| top_k = payload.get("top_k", 5) | ||||||
| kb_manager = self.get_kb_manager() | ||||||
| results = await kb_manager.retrieve( | ||||||
| query=query, | ||||||
| kb_names=kb_names, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3347,7 +3347,7 @@ paths: | |
| content: | ||
| application/json: | ||
| schema: | ||
| $ref: "#/components/schemas/KnowledgeBaseRequest" | ||
| $ref: "#/components/schemas/KnowledgeBaseCreateRequest" | ||
| responses: | ||
| "200": | ||
| $ref: "#/components/responses/Ok" | ||
|
|
@@ -5623,31 +5623,48 @@ components: | |
|
|
||
| KnowledgeBaseRequest: | ||
| type: object | ||
| required: [name] | ||
| properties: | ||
| name: | ||
| kb_name: | ||
| type: string | ||
| description: | ||
| type: string | ||
| embedding_provider_id: | ||
| emoji: | ||
| type: string | ||
| embedding_provider_id: | ||
| type: [string, "null"] | ||
| rerank_provider_id: | ||
| type: string | ||
| chunking: | ||
| $ref: "#/components/schemas/DynamicConfig" | ||
| metadata: | ||
| $ref: "#/components/schemas/DynamicConfig" | ||
| type: [string, "null"] | ||
| chunk_size: | ||
| type: integer | ||
| chunk_overlap: | ||
| type: integer | ||
| top_k_dense: | ||
| type: integer | ||
| top_k_sparse: | ||
| type: integer | ||
| top_m_final: | ||
| type: integer | ||
| additionalProperties: false | ||
|
|
||
| KnowledgeBaseCreateRequest: | ||
| allOf: | ||
| - $ref: "#/components/schemas/KnowledgeBaseRequest" | ||
| - type: object | ||
| required: [kb_name, embedding_provider_id] | ||
| properties: | ||
| kb_name: | ||
| type: string | ||
| embedding_provider_id: | ||
| type: string | ||
| additionalProperties: false | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In OpenAPI, combining Please remove KnowledgeBaseCreateRequest:
allOf:
- $ref: "#/components/schemas/KnowledgeBaseRequest"
- type: object
required: [kb_name, embedding_provider_id]
properties:
kb_name:
type: string
embedding_provider_id:
type: string |
||
|
|
||
| KnowledgeDocumentUploadRequest: | ||
| type: object | ||
| required: [file] | ||
| properties: | ||
| file: | ||
| type: string | ||
| format: binary | ||
| parser: | ||
| type: string | ||
|
|
||
| KnowledgeDocumentImportRequest: | ||
| type: object | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The helper function
_model_dictwas removed from this file. However, it is still referenced in multiple other endpoints within the same file:import_knowledge_base_documents(line 210)import_knowledge_base_document_url(line 224)retrieve_knowledge_base(line 304)Removing
_model_dictwill cause aNameErrorwhen any of these endpoints are called. Please restore_model_dictor refactor those endpoints to usepayload.model_dump(exclude_none=True)directly.