feat: add knowledge management API and permissions

This commit is contained in:
CaptainB 2025-04-25 17:06:40 +08:00
parent 83ace97ecc
commit 724686762e
13 changed files with 234 additions and 40 deletions

View File

@ -153,8 +153,11 @@ class PermissionConstants(Enum):
KNOWLEDGE_MODULE_EDIT = Permission(group=Group.KNOWLEDGE, operate=Operate.EDIT, role_list=[RoleConstants.ADMIN,
RoleConstants.USER])
KNOWLEDGE_MODULE_DELETE = Permission(group=Group.KNOWLEDGE, operate=Operate.DELETE, role_list=[RoleConstants.ADMIN,
RoleConstants.USER])
RoleConstants.USER])
KNOWLEDGE_READ = Permission(group=Group.KNOWLEDGE, operate=Operate.READ, role_list=[RoleConstants.ADMIN,
RoleConstants.USER])
KNOWLEDGE_CREATE = Permission(group=Group.KNOWLEDGE, operate=Operate.CREATE, role_list=[RoleConstants.ADMIN,
RoleConstants.USER])
def get_workspace_application_permission(self):
return lambda r, kwargs: Permission(group=self.value.group, operate=self.value.operate,
resource_path=

View File

View File

@ -0,0 +1,54 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from common.mixins.api_mixin import APIMixin
from common.result import ResultSerializer
from knowledge.serializers.knowledge import KnowledgeBaseCreateRequest, KnowledgeModelSerializer
class KnowledgeCreateResponse(ResultSerializer):
def get_data(self):
return KnowledgeModelSerializer()
class KnowledgeCreateAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
)
]
@staticmethod
def get_request():
return KnowledgeBaseCreateRequest
@staticmethod
def get_response():
return KnowledgeCreateResponse
class KnowledgeTreeReadAPI(APIMixin):
@staticmethod
def get_parameters():
return [
OpenApiParameter(
name="workspace_id",
description="工作空间id",
type=OpenApiTypes.STR,
location='path',
required=True,
),
OpenApiParameter(
name="module_id",
description="模块id",
type=OpenApiTypes.STR,
location='query',
required=False,
)
]

View File

@ -14,9 +14,7 @@ def insert_default_data(apps, schema_editor):
KnowledgeModule.objects.create(id='root', name='根目录', user_id='f0dd8f71-e4ee-11ee-8c84-a8a1595801ab')
class Migration(migrations.Migration):
initial = True
dependencies = [
@ -30,8 +28,12 @@ class Migration(migrations.Migration):
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('id',
models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False,
verbose_name='主键id')),
('file_name', models.CharField(default='', max_length=256, verbose_name='文件名称')),
('workspace_id',
models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')),
('loid', models.IntegerField(verbose_name='loid')),
('meta', models.JSONField(default=dict, verbose_name='文件关联数据')),
],
@ -39,41 +41,62 @@ class Migration(migrations.Migration):
'db_table': 'file',
},
),
migrations.CreateModel(
name='Knowledge',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False, verbose_name='主键id')),
('name', models.CharField(max_length=150, verbose_name='知识库名称')),
('desc', models.CharField(max_length=256, verbose_name='描述')),
('type', models.IntegerField(choices=[(0, '通用类型'), (1, 'web站点类型'), (2, '飞书类型'), (3, '语雀类型')], default=0, verbose_name='类型')),
('meta', models.JSONField(default=dict, verbose_name='元数据')),
('embedding_mode', models.ForeignKey(default=knowledge.models.knowledge.default_model, on_delete=django.db.models.deletion.DO_NOTHING, to='models_provider.model', verbose_name='向量模型')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='users.user', verbose_name='所属用户')),
],
options={
'db_table': 'knowledge',
},
),
migrations.CreateModel(
name='KnowledgeModule',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id', models.CharField(editable=False, max_length=64, primary_key=True, serialize=False, verbose_name='主键id')),
('id', models.CharField(editable=False, max_length=64, primary_key=True, serialize=False,
verbose_name='主键id')),
('name', models.CharField(max_length=64, verbose_name='文件夹名称')),
('workspace_id', models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')),
('workspace_id',
models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')),
('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(editable=False)),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='knowledge.knowledgemodule')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='用户id')),
('parent',
mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='children', to='knowledge.knowledgemodule')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='users.user',
verbose_name='用户id')),
],
options={
'db_table': 'knowledge_module',
},
),
migrations.CreateModel(
name='Knowledge',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
('id',
models.UUIDField(default=uuid_utils.compat.uuid7, editable=False, primary_key=True, serialize=False,
verbose_name='主键id')),
('name', models.CharField(max_length=150, verbose_name='知识库名称')),
('workspace_id',
models.CharField(db_index=True, default='default', max_length=64, verbose_name='工作空间id')),
('desc', models.CharField(max_length=256, verbose_name='描述')),
('type',
models.IntegerField(choices=[(0, '通用类型'), (1, 'web站点类型'), (2, '飞书类型'), (3, '语雀类型')],
default=0, verbose_name='类型')),
('meta', models.JSONField(default=dict, verbose_name='元数据')),
('scope',
models.CharField(choices=[('SHARED', '共享'), ('WORKSPACE', '工作空间可用')], default='WORKSPACE',
max_length=20, verbose_name='可用范围')),
('module',
models.ForeignKey(default='root', on_delete=django.db.models.deletion.CASCADE,
to='knowledge.knowledgemodule',
verbose_name='模块id')),
('embedding_model', models.ForeignKey(default=knowledge.models.knowledge.default_model,
on_delete=django.db.models.deletion.DO_NOTHING,
to='models_provider.model', verbose_name='向量模型')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='users.user',
verbose_name='所属用户')),
],
options={
'db_table': 'knowledge',
},
),
migrations.RunPython(insert_default_data),
]

View File

@ -12,10 +12,15 @@ from users.models import User
class KnowledgeType(models.IntegerChoices):
base = 0, '通用类型'
web = 1, 'web站点类型'
lark = 2, '飞书类型'
yuque = 3, '语雀类型'
BASE = 0, '通用类型'
WEB = 1, 'web站点类型'
LARK = 2, '飞书类型'
YUQUE = 3, '语雀类型'
class KnowledgeScope(models.TextChoices):
SHARED = "SHARED", '共享'
WORKSPACE = "WORKSPACE", "工作空间可用"
def default_model():
@ -26,7 +31,7 @@ def default_model():
class KnowledgeModule(MPTTModel, AppModelMixin):
id = models.CharField(primary_key=True, max_length=64, editable=False, verbose_name="主键id")
name = models.CharField(max_length=64, verbose_name="文件夹名称")
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户id")
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="用户id")
workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True)
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
@ -37,13 +42,17 @@ class KnowledgeModule(MPTTModel, AppModelMixin):
order_insertion_by = ['name']
class Knowledge(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
name = models.CharField(max_length=150, verbose_name="知识库名称")
workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True)
desc = models.CharField(max_length=256, verbose_name="描述")
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="所属用户")
type = models.IntegerField(verbose_name='类型', choices=KnowledgeType.choices, default=KnowledgeType.base)
embedding_mode = models.ForeignKey(Model, on_delete=models.DO_NOTHING, verbose_name="向量模型",
type = models.IntegerField(verbose_name='类型', choices=KnowledgeType.choices, default=KnowledgeType.BASE)
scope = models.CharField(max_length=20, verbose_name='可用范围', choices=KnowledgeScope.choices, default=KnowledgeScope.WORKSPACE)
module = models.ForeignKey(KnowledgeModule, on_delete=models.CASCADE, verbose_name="模块id", default='root')
embedding_model = models.ForeignKey(Model, on_delete=models.DO_NOTHING, verbose_name="向量模型",
default=default_model)
meta = models.JSONField(verbose_name="元数据", default=dict)
@ -54,6 +63,7 @@ class Knowledge(AppModelMixin):
class File(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
file_name = models.CharField(max_length=256, verbose_name="文件名称", default="")
workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True)
loid = models.IntegerField(verbose_name="loid")
meta = models.JSONField(verbose_name="文件关联数据", default=dict)

View File

@ -0,0 +1 @@
# coding=utf-8

View File

@ -0,0 +1,52 @@
import uuid_utils as uuid
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from knowledge.models import Knowledge, KnowledgeScope, KnowledgeType
class KnowledgeModelSerializer(serializers.ModelSerializer):
class Meta:
model = Knowledge
fields = ['id', 'name', 'desc', 'meta', 'module_id', 'type', 'workspace_id', 'create_time', 'update_time']
class KnowledgeBaseCreateRequest(serializers.Serializer):
name = serializers.CharField(required=True, label=_('knowledge name'))
desc = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('knowledge description'))
embedding = serializers.CharField(required=True, label=_('knowledge embedding'))
class KnowledgeWebCreateRequest(serializers.Serializer):
name = serializers.CharField(required=True, label=_('knowledge name'))
desc = serializers.CharField(required=False, allow_null=True, allow_blank=True, label=_('knowledge description'))
embedding = serializers.CharField(required=True, label=_('knowledge embedding'))
class KnowledgeSerializer(serializers.Serializer):
class Create(serializers.Serializer):
user_id = serializers.UUIDField(required=True, label=_('user id'))
workspace_id = serializers.CharField(required=True, label=_('workspace id'))
def insert(self, instance, with_valid=True):
if with_valid:
self.is_valid(raise_exception=True)
KnowledgeBaseCreateRequest(data=instance).is_valid(raise_exception=True)
knowledge = Knowledge(
id=uuid.uuid7(),
name=instance.get('name'),
workspace_id=self.data.get('workspace_id'),
desc=instance.get('desc'),
type=instance.get('type', KnowledgeType.BASE),
user_id=self.data.get('user_id'),
scope=KnowledgeScope.WORKSPACE,
module_id=instance.get('module_id', 'root'),
embedding_model_id=instance.get('embedding'),
meta=instance.get('meta', {}),
)
knowledge.save()
return KnowledgeModelSerializer(knowledge).data
class KnowledgeTreeSerializer(serializers.Serializer):
def get_knowledge_list(self, param):
pass

View File

@ -1,3 +1,8 @@
from django.shortcuts import render
from django.urls import path
# Create your views here.
from . import views
app_name = "knowledge"
urlpatterns = [
path('workspace/<str:workspace_id>/knowledge', views.KnowledgeView.as_view()),
]

View File

@ -0,0 +1 @@
from .knowledge import *

View File

@ -0,0 +1,44 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.views import APIView
from common.auth import TokenAuth
from common.auth.authentication import has_permissions
from common.constants.permission_constants import PermissionConstants
from common.result import result
from knowledge.api.knowledge import KnowledgeCreateAPI, KnowledgeTreeReadAPI
from knowledge.serializers.knowledge import KnowledgeSerializer, KnowledgeTreeSerializer
class KnowledgeView(APIView):
authentication_classes = [TokenAuth]
@extend_schema(
methods=['POST'],
description=_('Create knowledge'),
operation_id=_('Create knowledge'),
parameters=KnowledgeCreateAPI.get_parameters(),
request=KnowledgeCreateAPI.get_request(),
responses=KnowledgeCreateAPI.get_response(),
tags=[_('Knowledge Base')]
)
@has_permissions(PermissionConstants.KNOWLEDGE_CREATE.get_workspace_permission())
def post(self, request: Request, workspace_id: str):
return result.success(KnowledgeSerializer.Create(
data={'user_id': request.user.id, 'workspace_id': workspace_id}
).insert(request.data))
@extend_schema(
methods=['GET'],
description=_('Get knowledge by module'),
operation_id=_('Get knowledge by module'),
parameters=KnowledgeTreeReadAPI.get_parameters(),
responses=KnowledgeTreeReadAPI.get_response(),
tags=[_('Knowledge Base')]
)
@has_permissions(PermissionConstants.KNOWLEDGE_READ.get_workspace_permission())
def get(self, request: Request, workspace_id: str):
return result.success(KnowledgeTreeSerializer(
data={'workspace_id': workspace_id}
).get_knowledge_list(request.query_params.get('module_id')))

View File

@ -24,7 +24,8 @@ urlpatterns = [
path("api/", include("users.urls")),
path("api/", include("tools.urls")),
path("api/", include("models_provider.urls")),
path("api/", include("modules.urls"))
path("api/", include("modules.urls")),
path("api/", include("knowledge.urls")),
]
urlpatterns += [
path('schema/', SpectacularAPIView.as_view(), name='schema'), # schema的配置文件的路由下面两个ui也是根据这个配置文件来生成的

View File

@ -38,7 +38,7 @@ class Migration(migrations.Migration):
('parent',
mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='children', to='tools.toolmodule')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user',
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='users.user',
verbose_name='用户id')),
],
options={
@ -70,7 +70,7 @@ class Migration(migrations.Migration):
('template_id', models.UUIDField(default=None, null=True, verbose_name='模版id')),
('workspace_id', models.CharField(default='default', max_length=64, verbose_name='工作空间id', db_index=True)),
('init_params', models.CharField(max_length=102400, null=True, verbose_name='初始化参数')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user',
('user', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='users.user',
verbose_name='用户id')),
('module',
models.ForeignKey(default='root', on_delete=django.db.models.deletion.CASCADE, to='tools.toolmodule',

View File

@ -10,7 +10,7 @@ from users.models import User
class ToolModule(MPTTModel, AppModelMixin):
id = models.CharField(primary_key=True, max_length=64, editable=False, verbose_name="主键id")
name = models.CharField(max_length=64, verbose_name="文件夹名称")
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户id")
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="用户id")
workspace_id = models.CharField(max_length=64, verbose_name="工作空间id", default="default", db_index=True)
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
@ -33,7 +33,7 @@ class ToolType(models.TextChoices):
class Tool(AppModelMixin):
id = models.UUIDField(primary_key=True, max_length=128, default=uuid.uuid7, editable=False, verbose_name="主键id")
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户id")
user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="用户id")
name = models.CharField(max_length=64, verbose_name="工具名称")
desc = models.CharField(max_length=128, verbose_name="描述")
code = models.CharField(max_length=102400, verbose_name="python代码")