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.system",
|
||||||
"apps.tickets",
|
"apps.tickets",
|
||||||
"apps.solutions",
|
"apps.solutions",
|
||||||
|
"apps.knowledge",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from ninja import NinjaAPI
|
|||||||
|
|
||||||
from apps.accounts.api import router as accounts_router
|
from apps.accounts.api import router as accounts_router
|
||||||
from apps.audit.api import router as audit_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.solutions.api import router as solutions_router
|
||||||
from apps.system.api import router as system_router
|
from apps.system.api import router as system_router
|
||||||
from apps.tickets.api import router as tickets_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("/audit", audit_router)
|
||||||
api.add_router("/tickets", tickets_router)
|
api.add_router("/tickets", tickets_router)
|
||||||
api.add_router("/solutions", solutions_router)
|
api.add_router("/solutions", solutions_router)
|
||||||
|
api.add_router("/knowledge", knowledge_router)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
|||||||
Reference in New Issue
Block a user