feat: add knowledge base core api
This commit is contained in:
0
backend/apps/knowledge/__init__.py
Normal file
0
backend/apps/knowledge/__init__.py
Normal file
83
backend/apps/knowledge/api.py
Normal file
83
backend/apps/knowledge/api.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from ninja import Router
|
||||
|
||||
from apps.accounts.api import require_permission
|
||||
from apps.knowledge.models import KnowledgeBase, KnowledgeItem
|
||||
from apps.knowledge.schemas import (
|
||||
KnowledgeBaseIn,
|
||||
KnowledgeBaseOut,
|
||||
KnowledgeDocumentIn,
|
||||
KnowledgeDocumentOut,
|
||||
KnowledgeItemIn,
|
||||
KnowledgeItemOut,
|
||||
)
|
||||
from apps.knowledge.services import ingest_document_text, rebuild_item_chunks
|
||||
|
||||
router = Router(tags=["knowledge"])
|
||||
|
||||
|
||||
@router.post("/bases", response=KnowledgeBaseOut)
|
||||
def create_knowledge_base(request, payload: KnowledgeBaseIn):
|
||||
require_permission(request, "knowledge.manage")
|
||||
return KnowledgeBase.objects.create(
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
scope=payload.scope,
|
||||
enabled=payload.enabled,
|
||||
created_by=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/bases", response=list[KnowledgeBaseOut])
|
||||
def list_knowledge_bases(request, enabled: bool | None = None):
|
||||
require_permission(request, "knowledge.view")
|
||||
queryset = KnowledgeBase.objects.all()
|
||||
if enabled is not None:
|
||||
queryset = queryset.filter(enabled=enabled)
|
||||
return queryset[:100]
|
||||
|
||||
|
||||
@router.post("/items", response=KnowledgeItemOut)
|
||||
def create_knowledge_item(request, payload: KnowledgeItemIn):
|
||||
require_permission(request, "knowledge.manage")
|
||||
item = KnowledgeItem.objects.create(
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
title=payload.title,
|
||||
problem=payload.problem,
|
||||
applicable_scope=payload.applicable_scope,
|
||||
error_code=payload.error_code,
|
||||
root_cause=payload.root_cause,
|
||||
troubleshooting_steps=payload.troubleshooting_steps,
|
||||
solution=payload.solution,
|
||||
risk_note=payload.risk_note,
|
||||
status=payload.status,
|
||||
created_by=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
rebuild_item_chunks(item)
|
||||
return item
|
||||
|
||||
|
||||
@router.get("/items", response=list[KnowledgeItemOut])
|
||||
def list_knowledge_items(
|
||||
request,
|
||||
knowledge_base_id: int | None = None,
|
||||
status: str | None = None,
|
||||
):
|
||||
require_permission(request, "knowledge.view")
|
||||
queryset = KnowledgeItem.objects.all()
|
||||
if knowledge_base_id:
|
||||
queryset = queryset.filter(knowledge_base_id=knowledge_base_id)
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
return queryset[:100]
|
||||
|
||||
|
||||
@router.post("/documents", response=KnowledgeDocumentOut)
|
||||
def create_knowledge_document(request, payload: KnowledgeDocumentIn):
|
||||
require_permission(request, "knowledge.manage")
|
||||
return ingest_document_text(
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
file_name=payload.file_name,
|
||||
file_type=payload.file_type,
|
||||
content=payload.content,
|
||||
operator=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
6
backend/apps/knowledge/apps.py
Normal file
6
backend/apps/knowledge/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KnowledgeConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.knowledge"
|
||||
97
backend/apps/knowledge/migrations/0001_initial.py
Normal file
97
backend/apps/knowledge/migrations/0001_initial.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# ruff: noqa: E501
|
||||
# Generated by Django 5.2.15 on 2026-07-03 09:48
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KnowledgeBase',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=160)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('scope', models.CharField(blank=True, max_length=120)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-updated_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='KnowledgeDocument',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file_name', models.CharField(max_length=255)),
|
||||
('file_type', models.CharField(blank=True, max_length=80)),
|
||||
('storage_path', models.CharField(blank=True, max_length=500)),
|
||||
('content_text', models.TextField(blank=True)),
|
||||
('parse_status', models.CharField(default='pending', max_length=40)),
|
||||
('parse_error', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='knowledge.knowledgebase')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='KnowledgeItem',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('problem', models.TextField()),
|
||||
('applicable_scope', models.TextField(blank=True)),
|
||||
('error_code', models.CharField(blank=True, max_length=120)),
|
||||
('root_cause', models.TextField(blank=True)),
|
||||
('troubleshooting_steps', models.TextField(blank=True)),
|
||||
('solution', models.TextField()),
|
||||
('risk_note', models.TextField(blank=True)),
|
||||
('status', models.CharField(default='draft', max_length=40)),
|
||||
('source_type', models.CharField(default='manual', max_length=40)),
|
||||
('source_ticket_id', models.BigIntegerField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_knowledge_items', to=settings.AUTH_USER_MODEL)),
|
||||
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='knowledge.knowledgebase')),
|
||||
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_knowledge_items', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-updated_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='KnowledgeChunk',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('chunk_index', models.PositiveIntegerField(default=0)),
|
||||
('content', models.TextField()),
|
||||
('embedding', models.JSONField(blank=True, default=list)),
|
||||
('embedding_model', models.CharField(default='local-hash-v1', max_length=120)),
|
||||
('metadata', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('knowledge_base', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='knowledge.knowledgebase')),
|
||||
('document', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='knowledge.knowledgedocument')),
|
||||
('item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chunks', to='knowledge.knowledgeitem')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['knowledge_base_id', 'document_id', 'item_id', 'chunk_index'],
|
||||
'indexes': [models.Index(fields=['knowledge_base', 'chunk_index'], name='knowledge_k_knowled_949c7b_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
backend/apps/knowledge/migrations/__init__.py
Normal file
0
backend/apps/knowledge/migrations/__init__.py
Normal file
120
backend/apps/knowledge/models.py
Normal file
120
backend/apps/knowledge/models.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class KnowledgeBase(models.Model):
|
||||
name = models.CharField(max_length=160)
|
||||
description = models.TextField(blank=True)
|
||||
scope = models.CharField(max_length=120, blank=True)
|
||||
enabled = models.BooleanField(default=True)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-updated_at"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class KnowledgeDocument(models.Model):
|
||||
knowledge_base = models.ForeignKey(
|
||||
KnowledgeBase,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="documents",
|
||||
)
|
||||
file_name = models.CharField(max_length=255)
|
||||
file_type = models.CharField(max_length=80, blank=True)
|
||||
storage_path = models.CharField(max_length=500, blank=True)
|
||||
content_text = models.TextField(blank=True)
|
||||
parse_status = models.CharField(max_length=40, default="pending")
|
||||
parse_error = models.TextField(blank=True)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
|
||||
|
||||
class KnowledgeItem(models.Model):
|
||||
knowledge_base = models.ForeignKey(
|
||||
KnowledgeBase,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="items",
|
||||
)
|
||||
title = models.CharField(max_length=200)
|
||||
problem = models.TextField()
|
||||
applicable_scope = models.TextField(blank=True)
|
||||
error_code = models.CharField(max_length=120, blank=True)
|
||||
root_cause = models.TextField(blank=True)
|
||||
troubleshooting_steps = models.TextField(blank=True)
|
||||
solution = models.TextField()
|
||||
risk_note = models.TextField(blank=True)
|
||||
status = models.CharField(max_length=40, default="draft")
|
||||
source_type = models.CharField(max_length=40, default="manual")
|
||||
source_ticket_id = models.BigIntegerField(null=True, blank=True)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="created_knowledge_items",
|
||||
)
|
||||
reviewed_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="reviewed_knowledge_items",
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-updated_at"]
|
||||
|
||||
|
||||
class KnowledgeChunk(models.Model):
|
||||
knowledge_base = models.ForeignKey(
|
||||
KnowledgeBase,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="chunks",
|
||||
)
|
||||
document = models.ForeignKey(
|
||||
KnowledgeDocument,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="chunks",
|
||||
)
|
||||
item = models.ForeignKey(
|
||||
KnowledgeItem,
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="chunks",
|
||||
)
|
||||
chunk_index = models.PositiveIntegerField(default=0)
|
||||
content = models.TextField()
|
||||
embedding = models.JSONField(default=list, blank=True)
|
||||
embedding_model = models.CharField(max_length=120, default="local-hash-v1")
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["knowledge_base_id", "document_id", "item_id", "chunk_index"]
|
||||
indexes = [
|
||||
models.Index(fields=["knowledge_base", "chunk_index"]),
|
||||
]
|
||||
67
backend/apps/knowledge/schemas.py
Normal file
67
backend/apps/knowledge/schemas.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from datetime import datetime
|
||||
|
||||
from ninja import Schema
|
||||
|
||||
|
||||
class KnowledgeBaseIn(Schema):
|
||||
name: str
|
||||
description: str = ""
|
||||
scope: str = ""
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class KnowledgeBaseOut(Schema):
|
||||
id: int
|
||||
name: str
|
||||
description: str
|
||||
scope: str
|
||||
enabled: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class KnowledgeDocumentIn(Schema):
|
||||
knowledge_base_id: int
|
||||
file_name: str
|
||||
file_type: str = ""
|
||||
content: str
|
||||
|
||||
|
||||
class KnowledgeDocumentOut(Schema):
|
||||
id: int
|
||||
knowledge_base_id: int
|
||||
file_name: str
|
||||
file_type: str
|
||||
parse_status: str
|
||||
parse_error: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class KnowledgeItemIn(Schema):
|
||||
knowledge_base_id: int
|
||||
title: str
|
||||
problem: str
|
||||
applicable_scope: str = ""
|
||||
error_code: str = ""
|
||||
root_cause: str = ""
|
||||
troubleshooting_steps: str = ""
|
||||
solution: str
|
||||
risk_note: str = ""
|
||||
status: str = "draft"
|
||||
|
||||
|
||||
class KnowledgeItemOut(Schema):
|
||||
id: int
|
||||
knowledge_base_id: int
|
||||
title: str
|
||||
problem: str
|
||||
applicable_scope: str
|
||||
error_code: str
|
||||
root_cause: str
|
||||
troubleshooting_steps: str
|
||||
solution: str
|
||||
risk_note: str
|
||||
status: str
|
||||
source_type: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
87
backend/apps/knowledge/services.py
Normal file
87
backend/apps/knowledge/services.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import hashlib
|
||||
import math
|
||||
|
||||
from apps.knowledge.models import KnowledgeChunk, KnowledgeDocument, KnowledgeItem
|
||||
|
||||
DEFAULT_CHUNK_SIZE = 600
|
||||
|
||||
|
||||
def split_text(content: str, chunk_size: int = DEFAULT_CHUNK_SIZE) -> list[str]:
|
||||
normalized = "\n".join(line.strip() for line in content.splitlines() if line.strip())
|
||||
if not normalized:
|
||||
return []
|
||||
return [
|
||||
normalized[index : index + chunk_size]
|
||||
for index in range(0, len(normalized), chunk_size)
|
||||
]
|
||||
|
||||
|
||||
def embed_text(content: str, dimensions: int = 16) -> list[float]:
|
||||
digest = hashlib.sha256(content.encode("utf-8")).digest()
|
||||
values = [digest[index] / 255 for index in range(dimensions)]
|
||||
length = math.sqrt(sum(value * value for value in values)) or 1
|
||||
return [round(value / length, 6) for value in values]
|
||||
|
||||
|
||||
def rebuild_item_chunks(item: KnowledgeItem) -> list[KnowledgeChunk]:
|
||||
item.chunks.all().delete()
|
||||
content = "\n".join(
|
||||
[
|
||||
item.title,
|
||||
item.problem,
|
||||
item.applicable_scope,
|
||||
item.error_code,
|
||||
item.root_cause,
|
||||
item.troubleshooting_steps,
|
||||
item.solution,
|
||||
item.risk_note,
|
||||
]
|
||||
)
|
||||
chunks = []
|
||||
for index, chunk_text in enumerate(split_text(content)):
|
||||
chunks.append(
|
||||
KnowledgeChunk.objects.create(
|
||||
knowledge_base=item.knowledge_base,
|
||||
item=item,
|
||||
chunk_index=index,
|
||||
content=chunk_text,
|
||||
embedding=embed_text(chunk_text),
|
||||
metadata={
|
||||
"source_type": "knowledge_item",
|
||||
"title": item.title,
|
||||
"item_id": item.id,
|
||||
},
|
||||
)
|
||||
)
|
||||
return chunks
|
||||
|
||||
|
||||
def ingest_document_text(
|
||||
knowledge_base_id: int,
|
||||
file_name: str,
|
||||
file_type: str,
|
||||
content: str,
|
||||
operator=None,
|
||||
) -> KnowledgeDocument:
|
||||
document = KnowledgeDocument.objects.create(
|
||||
knowledge_base_id=knowledge_base_id,
|
||||
file_name=file_name,
|
||||
file_type=file_type,
|
||||
content_text=content,
|
||||
parse_status="success",
|
||||
created_by=operator,
|
||||
)
|
||||
for index, chunk_text in enumerate(split_text(content)):
|
||||
KnowledgeChunk.objects.create(
|
||||
knowledge_base=document.knowledge_base,
|
||||
document=document,
|
||||
chunk_index=index,
|
||||
content=chunk_text,
|
||||
embedding=embed_text(chunk_text),
|
||||
metadata={
|
||||
"source_type": "document",
|
||||
"title": document.file_name,
|
||||
"document_id": document.id,
|
||||
},
|
||||
)
|
||||
return document
|
||||
0
backend/apps/knowledge/tests/__init__.py
Normal file
0
backend/apps/knowledge/tests/__init__.py
Normal file
69
backend/apps/knowledge/tests/test_knowledge_api.py
Normal file
69
backend/apps/knowledge/tests/test_knowledge_api.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from apps.knowledge.models import KnowledgeBase, KnowledgeChunk, KnowledgeDocument, KnowledgeItem
|
||||
|
||||
|
||||
def test_create_and_list_knowledge_base(admin_client, db):
|
||||
response = admin_client.post(
|
||||
"/api/knowledge/bases",
|
||||
data={
|
||||
"name": "税务采集知识库",
|
||||
"description": "税局采集、日志和常见问题",
|
||||
"scope": "数据采集",
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["name"] == "税务采集知识库"
|
||||
assert payload["enabled"] is True
|
||||
|
||||
response = admin_client.get("/api/knowledge/bases")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()[0]["name"] == "税务采集知识库"
|
||||
|
||||
|
||||
def test_create_knowledge_item_builds_chunks(admin_client, db):
|
||||
base = KnowledgeBase.objects.create(name="税务采集知识库", scope="数据采集")
|
||||
|
||||
response = admin_client.post(
|
||||
"/api/knowledge/items",
|
||||
data={
|
||||
"knowledge_base_id": base.id,
|
||||
"title": "税局采集进度查询",
|
||||
"problem": "客户询问采集进度长时间不更新",
|
||||
"applicable_scope": "生产环境税局采集",
|
||||
"error_code": "",
|
||||
"root_cause": "税局服务响应不稳定",
|
||||
"troubleshooting_steps": "查询 Python 日志并确认采集任务状态",
|
||||
"solution": "告知客户税局不稳定,任务仍在采集中,稍后复查。",
|
||||
"risk_note": "不要重复触发采集任务。",
|
||||
"status": "published",
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
item = KnowledgeItem.objects.get(id=response.json()["id"])
|
||||
assert item.status == "published"
|
||||
assert KnowledgeChunk.objects.filter(item=item).count() >= 1
|
||||
|
||||
|
||||
def test_ingest_document_text_creates_document_and_chunks(admin_client, db):
|
||||
base = KnowledgeBase.objects.create(name="日志知识库", scope="技术支持")
|
||||
|
||||
response = admin_client.post(
|
||||
"/api/knowledge/documents",
|
||||
data={
|
||||
"knowledge_base_id": base.id,
|
||||
"file_name": "tax-log.md",
|
||||
"file_type": "markdown",
|
||||
"content": "税局接口不稳定时,采集任务会停留在处理中。需要查询 Python 日志确认。",
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
document = KnowledgeDocument.objects.get(id=response.json()["id"])
|
||||
assert document.parse_status == "success"
|
||||
assert KnowledgeChunk.objects.filter(document=document).count() >= 1
|
||||
@@ -20,6 +20,7 @@ INSTALLED_APPS = [
|
||||
"apps.system",
|
||||
"apps.tickets",
|
||||
"apps.solutions",
|
||||
"apps.knowledge",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -4,6 +4,7 @@ from ninja import NinjaAPI
|
||||
|
||||
from apps.accounts.api import router as accounts_router
|
||||
from apps.audit.api import router as audit_router
|
||||
from apps.knowledge.api import router as knowledge_router
|
||||
from apps.solutions.api import router as solutions_router
|
||||
from apps.system.api import router as system_router
|
||||
from apps.tickets.api import router as tickets_router
|
||||
@@ -14,6 +15,7 @@ api.add_router("/system", system_router)
|
||||
api.add_router("/audit", audit_router)
|
||||
api.add_router("/tickets", tickets_router)
|
||||
api.add_router("/solutions", solutions_router)
|
||||
api.add_router("/knowledge", knowledge_router)
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
|
||||
Reference in New Issue
Block a user