220 lines
7.9 KiB
Python
220 lines
7.9 KiB
Python
"""Project Knowledge Graph API."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from ..core.response import success_response
|
|
from ..database import get_db
|
|
from ..dependencies.auth import get_current_user
|
|
from ..dependencies.tenant import get_tenant_id
|
|
from ..models import User
|
|
from ..services.auth import has_any_role
|
|
from ..services.model_context_bundle_service import ModelContextBundleService
|
|
from ..services.project_knowledge_graph import ProjectKnowledgeGraphService
|
|
|
|
router = APIRouter(prefix="/ai-steward/knowledge-graph", tags=["AI Steward"])
|
|
|
|
|
|
class StructuredSyncRequest(BaseModel):
|
|
limit: int = Field(default=1000, ge=1, le=5000)
|
|
|
|
|
|
class CodeScanRequest(BaseModel):
|
|
base_dir: Optional[str] = Field(default=None, max_length=400)
|
|
|
|
|
|
class FeishuDeltaEnqueueRequest(BaseModel):
|
|
limit: int = Field(default=500, ge=1, le=2000)
|
|
|
|
|
|
class EventProcessRequest(BaseModel):
|
|
max_batch: int = Field(default=200, ge=1, le=1000)
|
|
max_retry: int = Field(default=3, ge=0, le=10)
|
|
|
|
|
|
class IncrementalCycleRequest(BaseModel):
|
|
delta_limit: int = Field(default=1000, ge=1, le=5000)
|
|
event_batch: int = Field(default=200, ge=1, le=1000)
|
|
code_scan_hours: int = Field(default=6, ge=1, le=168)
|
|
code_scan_base_dir: Optional[str] = Field(default=None, max_length=400)
|
|
|
|
|
|
class VerifyContextConsumptionRequest(BaseModel):
|
|
run_canary: bool = Field(default=True)
|
|
|
|
|
|
def _require_manager(user: User) -> None:
|
|
if not has_any_role(str(user.role or ""), ["BOSS", "ADMIN"]):
|
|
raise HTTPException(status_code=403, detail="仅管理员可操作知识图谱同步")
|
|
|
|
|
|
@router.get("/overview")
|
|
async def get_kg_overview(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_ = current_user
|
|
service = ProjectKnowledgeGraphService(db, tenant_id=int(getattr(current_user, "tenant_id", tenant_id) or tenant_id))
|
|
data = await service.get_overview()
|
|
return success_response(data)
|
|
|
|
|
|
@router.get("/sync-runs")
|
|
async def list_sync_runs(
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
run_type: Optional[str] = Query(default=None, max_length=40),
|
|
status: Optional[str] = Query(default=None, max_length=20),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_require_manager(current_user)
|
|
service = ProjectKnowledgeGraphService(db, tenant_id=int(getattr(current_user, "tenant_id", tenant_id) or tenant_id))
|
|
data = await service.list_sync_runs(
|
|
limit=int(limit),
|
|
run_type=run_type,
|
|
status=status,
|
|
)
|
|
return success_response(data)
|
|
|
|
|
|
@router.get("/sync-slo")
|
|
async def get_sync_slo(
|
|
lookback_hours: int = Query(default=24, ge=1, le=24 * 30),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_require_manager(current_user)
|
|
service = ProjectKnowledgeGraphService(db, tenant_id=int(getattr(current_user, "tenant_id", tenant_id) or tenant_id))
|
|
data = await service.get_sync_slo(lookback_hours=int(lookback_hours))
|
|
return success_response(data)
|
|
|
|
|
|
@router.get("/capability-summary")
|
|
async def get_capability_summary(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
effective_tenant_id = int(getattr(current_user, "tenant_id", tenant_id) or tenant_id)
|
|
data = await ModelContextBundleService.build_capability_summary(
|
|
db,
|
|
tenant_id=effective_tenant_id,
|
|
)
|
|
return success_response(data)
|
|
|
|
|
|
@router.post("/verify-context-consumption")
|
|
async def verify_context_consumption(
|
|
body: Optional[VerifyContextConsumptionRequest] = None,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_require_manager(current_user)
|
|
effective_tenant_id = int(getattr(current_user, "tenant_id", tenant_id) or tenant_id)
|
|
data = await ModelContextBundleService.verify_context_consumption(
|
|
db,
|
|
tenant_id=effective_tenant_id,
|
|
run_canary=bool(body.run_canary) if body is not None else True,
|
|
)
|
|
return success_response(data)
|
|
|
|
|
|
@router.post("/sync/structured")
|
|
async def sync_structured(
|
|
body: StructuredSyncRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_require_manager(current_user)
|
|
service = ProjectKnowledgeGraphService(db, tenant_id=int(getattr(current_user, "tenant_id", tenant_id) or tenant_id))
|
|
data = await service.sync_structured_entities(limit=int(body.limit))
|
|
return success_response(data)
|
|
|
|
|
|
@router.post("/sync/codebase")
|
|
async def sync_codebase(
|
|
body: CodeScanRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_require_manager(current_user)
|
|
service = ProjectKnowledgeGraphService(db, tenant_id=int(getattr(current_user, "tenant_id", tenant_id) or tenant_id))
|
|
|
|
root = Path("/app/backend").resolve()
|
|
requested = Path(body.base_dir).resolve() if body.base_dir else root
|
|
# 仅允许扫描 backend 根目录或其子目录
|
|
if root not in requested.parents and requested != root:
|
|
raise HTTPException(status_code=400, detail="base_dir 仅允许 /app/backend 范围内路径")
|
|
|
|
try:
|
|
data = await service.sync_codebase_snapshot(base_dir=str(requested))
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return success_response(data)
|
|
|
|
|
|
@router.post("/sync/feishu")
|
|
async def enqueue_feishu_delta(
|
|
body: FeishuDeltaEnqueueRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_require_manager(current_user)
|
|
service = ProjectKnowledgeGraphService(db, tenant_id=int(getattr(current_user, "tenant_id", tenant_id) or tenant_id))
|
|
data = await service.enqueue_feishu_delta_events(limit=int(body.limit))
|
|
return success_response(data)
|
|
|
|
|
|
@router.post("/events/process")
|
|
async def process_ingest_events(
|
|
body: EventProcessRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_require_manager(current_user)
|
|
service = ProjectKnowledgeGraphService(db, tenant_id=int(getattr(current_user, "tenant_id", tenant_id) or tenant_id))
|
|
data = await service.process_pending_events(
|
|
max_batch=int(body.max_batch),
|
|
max_retry=int(body.max_retry),
|
|
)
|
|
return success_response(data)
|
|
|
|
|
|
@router.post("/sync/incremental")
|
|
async def run_incremental_cycle(
|
|
body: IncrementalCycleRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
tenant_id: int = Depends(get_tenant_id),
|
|
):
|
|
_require_manager(current_user)
|
|
service = ProjectKnowledgeGraphService(db, tenant_id=int(getattr(current_user, "tenant_id", tenant_id) or tenant_id))
|
|
|
|
root = Path("/app/backend").resolve()
|
|
requested = Path(body.code_scan_base_dir).resolve() if body.code_scan_base_dir else None
|
|
if requested is not None and root not in requested.parents and requested != root:
|
|
raise HTTPException(status_code=400, detail="code_scan_base_dir 仅允许 /app/backend 范围内路径")
|
|
|
|
data = await service.run_incremental_cycle(
|
|
delta_limit=int(body.delta_limit),
|
|
event_batch=int(body.event_batch),
|
|
code_scan_hours=int(body.code_scan_hours),
|
|
code_scan_base_dir=str(requested) if requested else None,
|
|
run_source="manual_api",
|
|
)
|
|
return success_response(data)
|