mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* feat(knowledge): add optional platform connections and hierarchical spaces * fix(knowledge): return actionable errors for unavailable policy storage * perf(knowledge): resolve space policy once per snapshot
134 lines
5.9 KiB
Python
134 lines
5.9 KiB
Python
"""Host-authenticated platform routes; no caller-provided identities or credentials."""
|
|
|
|
import logging
|
|
import sqlite3
|
|
from typing import Literal
|
|
|
|
from deerflow_extension_api.auth import arequire_plugin_management
|
|
from deerflow_extension_knowledge_management.contracts import IngestRequest, ManagementCredentialUnavailable, ProcessingOptions
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import ValidationError
|
|
|
|
from .models import Update
|
|
from .store import RevisionConflict, administrator
|
|
|
|
NAMESPACE = "knowledge-platform"
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def principal(request: Request):
|
|
try:
|
|
result = await arequire_plugin_management(request, NAMESPACE, scope="read" if request.method == "GET" else "write")
|
|
if result.is_internal or not result.user_id:
|
|
raise PermissionError
|
|
return result
|
|
except PermissionError:
|
|
raise HTTPException(403, "Knowledge platform access denied") from None
|
|
|
|
|
|
async def manager(request: Request, caller=Depends(principal)):
|
|
try:
|
|
administrator(caller)
|
|
await arequire_plugin_management(request, "knowledge-management", scope="read" if request.method == "GET" else "write")
|
|
return caller
|
|
except PermissionError:
|
|
raise HTTPException(403, "Knowledge management access denied") from None
|
|
|
|
|
|
async def invoke(awaitable):
|
|
try:
|
|
return await awaitable
|
|
except PermissionError:
|
|
raise HTTPException(403, "Space or provider access denied") from None
|
|
except RevisionConflict:
|
|
raise HTTPException(409, "Policy changed; reload and reconcile before retrying") from None
|
|
except FileNotFoundError:
|
|
raise HTTPException(404, "Provider resource unavailable") from None
|
|
except FileExistsError:
|
|
raise HTTPException(409, "Resource conflict; reconcile before retrying") from None
|
|
except NotImplementedError:
|
|
raise HTTPException(422, "Unsupported capability or processing option") from None
|
|
except ValueError:
|
|
raise HTTPException(422, "Invalid platform policy or request") from None
|
|
except ManagementCredentialUnavailable:
|
|
raise HTTPException(503, "Management credential is not configured") from None
|
|
except sqlite3.Error as exc:
|
|
logger.warning("Knowledge platform storage failed: %s", type(exc).__name__)
|
|
raise HTTPException(503, "Policy storage is busy or unavailable; reload policy before retrying") from None
|
|
except RuntimeError as exc:
|
|
logger.warning("Knowledge platform provider failed: %s", type(exc).__name__)
|
|
raise HTTPException(502, "Provider request failed; reconcile before retrying a write") from None
|
|
|
|
|
|
async def bounded(request, limit):
|
|
data = bytearray()
|
|
async for chunk in request.stream():
|
|
if len(data) + len(chunk) > limit:
|
|
raise HTTPException(413, "Platform request exceeds size limit")
|
|
data.extend(chunk)
|
|
return bytes(data)
|
|
|
|
|
|
async def body(request, model):
|
|
data = await bounded(request, 1024 * 1024)
|
|
try:
|
|
return model.model_validate_json(data)
|
|
except ValidationError:
|
|
raise HTTPException(422, "Invalid platform request") from None
|
|
|
|
|
|
def build_router(platform):
|
|
router = APIRouter(prefix="/api/extensions/knowledge-platform", tags=[NAMESPACE])
|
|
|
|
@router.get("/policy")
|
|
async def policy(caller=Depends(principal)):
|
|
return await invoke(platform.policy(caller))
|
|
|
|
@router.put("/policy")
|
|
async def replace(request: Request, caller=Depends(principal)):
|
|
if not caller.is_admin:
|
|
raise HTTPException(403, "Policy changes require an administrator")
|
|
update = await body(request, Update)
|
|
return await invoke(platform.replace(caller, update.policy, update.revision))
|
|
|
|
@router.get("/spaces")
|
|
async def spaces(caller=Depends(principal)):
|
|
return await invoke(platform.spaces(caller))
|
|
|
|
@router.get("/spaces/{space}")
|
|
async def effective(space: str, caller=Depends(principal)):
|
|
return await invoke(platform.effective(caller, space))
|
|
|
|
prefix = "/spaces/{space}/resources/{resource}"
|
|
|
|
@router.get(prefix + "/capabilities")
|
|
async def capabilities(space: str, resource: str, caller=Depends(manager)):
|
|
return await invoke(platform.invoke(caller, space, resource, "capabilities"))
|
|
|
|
@router.post(prefix + "/documents", status_code=202)
|
|
async def ingest(request: Request, space: str, resource: str, caller=Depends(manager)):
|
|
data = await body(request, IngestRequest)
|
|
return await invoke(platform.invoke(caller, space, resource, data.kind, data))
|
|
|
|
@router.post(prefix + "/files", status_code=202)
|
|
async def upload(request: Request, space: str, resource: str, filename: str, caller=Depends(manager)):
|
|
if request.headers.get("content-type", "").split(";", 1)[0].strip().lower() != "application/octet-stream":
|
|
raise HTTPException(415, "Use application/octet-stream")
|
|
try:
|
|
options = ProcessingOptions.model_validate_json(request.headers.get("x-processing-options", "{}"))
|
|
except ValidationError:
|
|
raise HTTPException(422, "Invalid processing options") from None
|
|
data = await bounded(request, 16 * 1024 * 1024)
|
|
return await invoke(platform.invoke(caller, space, resource, "upload", options, filename=filename, content=data))
|
|
|
|
@router.get(prefix + "/documents/{document}")
|
|
async def status(space: str, resource: str, document: str, caller=Depends(manager)):
|
|
return await invoke(platform.invoke(caller, space, resource, "status", document))
|
|
|
|
@router.post(prefix + "/documents/{document}/{action}", status_code=202)
|
|
async def process(request: Request, space: str, resource: str, document: str, action: Literal["reprocess", "cancel"], caller=Depends(manager)):
|
|
options = await body(request, ProcessingOptions)
|
|
return await invoke(platform.invoke(caller, space, resource, action, options, document=document))
|
|
|
|
return router
|