Scope knowledge and image libraries by brand
Frontend CI / Frontend (push) Successful in 2m59s
Backend CI / Backend (push) Successful in 14m58s

This commit is contained in:
2026-06-30 12:30:10 +08:00
parent 95ddb3057f
commit ced0c4ec0f
35 changed files with 1136 additions and 326 deletions
+178 -102
View File
@@ -61,6 +61,7 @@ type knowledgeParseJob struct {
type knowledgeItemRecord struct {
ID int64
TenantID int64
BrandID int64
GroupID int64
GroupName string
Name string
@@ -207,12 +208,16 @@ func (s *KnowledgeService) runtimeConfig() knowledgeRuntimeConfig {
func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResponse, error) {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, `
SELECT id, name, parent_id, sort_order, created_at
FROM knowledge_groups
WHERE tenant_id = $1 AND deleted_at IS NULL
WHERE tenant_id = $1 AND brand_id = $2 AND deleted_at IS NULL
ORDER BY sort_order, created_at, id
`, actor.TenantID)
`, actor.TenantID, brandID)
if err != nil {
return nil, response.ErrInternal(50050, "knowledge_groups_query_failed", "failed to list knowledge groups")
}
@@ -240,6 +245,7 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
SELECT ki.group_id, COUNT(DISTINCT ki.id)::bigint
FROM knowledge_items ki
WHERE ki.tenant_id = $1
AND ki.brand_id = $2
AND ki.deleted_at IS NULL
AND ki.status = 'completed'
AND EXISTS (
@@ -251,7 +257,7 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
AND kcm.status = 'active'
)
GROUP BY ki.group_id
`, actor.TenantID)
`, actor.TenantID, brandID)
if err != nil {
return nil, response.ErrInternal(50050, "knowledge_group_counts_query_failed", "failed to list knowledge group counts")
}
@@ -310,6 +316,10 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRequest) (*KnowledgeGroupResponse, error) {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return nil, err
}
name := strings.TrimSpace(req.Name)
if name == "" {
@@ -318,7 +328,7 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
var parentID *int64
if req.ParentID != nil && *req.ParentID > 0 {
if err := s.ensureRootGroup(ctx, actor.TenantID, *req.ParentID); err != nil {
if err := s.ensureRootGroup(ctx, actor.TenantID, brandID, *req.ParentID); err != nil {
return nil, err
}
parentID = req.ParentID
@@ -333,11 +343,11 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
groupID int64
createdAt time.Time
)
err := s.pool.QueryRow(ctx, `
INSERT INTO knowledge_groups (tenant_id, name, parent_id, sort_order)
VALUES ($1, $2, $3, $4)
err = s.pool.QueryRow(ctx, `
INSERT INTO knowledge_groups (tenant_id, brand_id, name, parent_id, sort_order)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, created_at
`, actor.TenantID, name, parentID, sortOrder).Scan(&groupID, &createdAt)
`, actor.TenantID, brandID, name, parentID, sortOrder).Scan(&groupID, &createdAt)
if err != nil {
return nil, response.ErrConflict(40950, "knowledge_group_create_failed", "knowledge group already exists or cannot be created")
}
@@ -355,6 +365,10 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req KnowledgeGroupRequest) error {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return err
}
name := strings.TrimSpace(req.Name)
if name == "" {
@@ -369,8 +383,8 @@ func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req K
tag, err := s.pool.Exec(ctx, `
UPDATE knowledge_groups
SET name = $1, sort_order = $2, updated_at = NOW()
WHERE id = $3 AND tenant_id = $4 AND deleted_at IS NULL
`, name, sortOrder, groupID, actor.TenantID)
WHERE id = $3 AND tenant_id = $4 AND brand_id = $5 AND deleted_at IS NULL
`, name, sortOrder, groupID, actor.TenantID, brandID)
if err != nil {
return response.ErrConflict(40950, "knowledge_group_update_failed", "knowledge group already exists or cannot be updated")
}
@@ -382,6 +396,10 @@ func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req K
func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return err
}
tx, err := s.pool.Begin(ctx)
if err != nil {
@@ -389,7 +407,7 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
}
defer func() { _ = tx.Rollback(ctx) }()
groupIDs, err := s.listGroupSubtreeIDs(ctx, tx, actor.TenantID, groupID)
groupIDs, err := s.listGroupSubtreeIDs(ctx, tx, actor.TenantID, brandID, groupID)
if err != nil {
return err
}
@@ -397,7 +415,7 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
return response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
}
itemIDs, err := s.listActiveKnowledgeItemIDs(ctx, tx, actor.TenantID, groupIDs)
itemIDs, err := s.listActiveKnowledgeItemIDs(ctx, tx, actor.TenantID, brandID, groupIDs)
if err != nil {
return err
}
@@ -407,9 +425,10 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
UPDATE knowledge_items
SET deleted_at = NOW(), updated_at = NOW(), status = 'deleted'
WHERE tenant_id = $1
AND brand_id = $2
AND deleted_at IS NULL
AND id = ANY($2::bigint[])
`, actor.TenantID, itemIDs); err != nil {
AND id = ANY($3::bigint[])
`, actor.TenantID, brandID, itemIDs); err != nil {
return response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to delete knowledge items")
}
@@ -448,9 +467,10 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
UPDATE knowledge_groups
SET deleted_at = NOW(), updated_at = NOW()
WHERE tenant_id = $1
AND brand_id = $2
AND deleted_at IS NULL
AND id = ANY($2::bigint[])
`, actor.TenantID, groupIDs); err != nil {
AND id = ANY($3::bigint[])
`, actor.TenantID, brandID, groupIDs); err != nil {
return response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to delete knowledge groups")
}
@@ -475,8 +495,12 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]KnowledgeItemResponse, error) {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return nil, err
}
if groupID != nil && *groupID > 0 {
if err := s.ensureGroupAccessible(ctx, actor.TenantID, *groupID); err != nil {
if err := s.ensureGroupAccessible(ctx, actor.TenantID, brandID, *groupID); err != nil {
return nil, err
}
}
@@ -485,23 +509,24 @@ func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]Kno
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
ki.size_bytes, ki.error_message, ki.created_at, ki.updated_at
FROM knowledge_items ki
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
WHERE ki.tenant_id = $1
AND ki.brand_id = $2
AND ki.deleted_at IS NULL
`
args := []any{actor.TenantID}
args := []any{actor.TenantID, brandID}
if groupID != nil && *groupID > 0 {
query += `
AND ki.group_id IN (
WITH RECURSIVE selected_groups AS (
SELECT id
FROM knowledge_groups
WHERE id = $2 AND tenant_id = $1 AND deleted_at IS NULL
WHERE id = $3 AND tenant_id = $1 AND brand_id = $2 AND deleted_at IS NULL
UNION ALL
SELECT child.id
FROM knowledge_groups child
JOIN selected_groups parent ON child.parent_id = parent.id
WHERE child.tenant_id = $1 AND child.deleted_at IS NULL
WHERE child.tenant_id = $1 AND child.brand_id = $2 AND child.deleted_at IS NULL
)
SELECT id FROM selected_groups
)
@@ -551,6 +576,10 @@ func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]Kno
func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*KnowledgeItemDetailResponse, error) {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return nil, err
}
var (
item KnowledgeItemDetailResponse
@@ -560,13 +589,13 @@ func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*Kn
createdAt, updatedAt time.Time
)
err := s.pool.QueryRow(ctx, `
err = s.pool.QueryRow(ctx, `
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
ki.size_bytes, ki.error_message, ki.markdown_content, ki.created_at, ki.updated_at
FROM knowledge_items ki
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NULL
`, itemID, actor.TenantID).Scan(
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.brand_id = $3 AND ki.deleted_at IS NULL
`, itemID, actor.TenantID, brandID).Scan(
&item.ID,
&item.GroupID,
&item.GroupName,
@@ -597,6 +626,10 @@ func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*Kn
func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeTextItemRequest) (*KnowledgeItemResponse, error) {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return nil, err
}
if err := s.validateIngestionDependencies("text"); err != nil {
return nil, err
@@ -612,7 +645,7 @@ func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeText
return nil, response.ErrBadRequest(40055, "knowledge_text_name_too_long", "knowledge text name must be at most 20 characters")
}
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
GroupID: req.GroupID,
Name: name,
SourceType: "text",
@@ -623,6 +656,10 @@ func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeText
func (s *KnowledgeService) CreateURLItem(ctx context.Context, req KnowledgeURLItemRequest) (*KnowledgeItemResponse, error) {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return nil, err
}
if err := s.validateIngestionDependencies("website"); err != nil {
return nil, err
@@ -633,7 +670,7 @@ func (s *KnowledgeService) CreateURLItem(ctx context.Context, req KnowledgeURLIt
return nil, response.ErrBadRequest(40052, "knowledge_url_invalid", "knowledge url is invalid")
}
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
GroupID: req.GroupID,
Name: strings.TrimSpace(req.Name),
SourceType: "website",
@@ -649,6 +686,10 @@ func (s *KnowledgeService) CreateFileItem(
content []byte,
) (*KnowledgeItemResponse, error) {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return nil, err
}
if err := s.validateIngestionDependencies("document"); err != nil {
return nil, err
@@ -664,7 +705,7 @@ func (s *KnowledgeService) CreateFileItem(
return nil, response.ErrServiceUnavailable(50357, "knowledge_storage_unavailable", err.Error())
}
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
GroupID: groupID,
Name: displayName,
SourceType: "document",
@@ -675,6 +716,10 @@ func (s *KnowledgeService) CreateFileItem(
func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return err
}
tx, err := s.pool.Begin(ctx)
if err != nil {
@@ -685,8 +730,8 @@ func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
tag, err := tx.Exec(ctx, `
UPDATE knowledge_items
SET deleted_at = NOW(), updated_at = NOW(), status = 'deleted'
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
`, itemID, actor.TenantID)
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
`, itemID, actor.TenantID, brandID)
if err != nil {
return response.ErrInternal(50054, "knowledge_item_delete_failed", "failed to delete knowledge item")
}
@@ -734,14 +779,18 @@ func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*KnowledgeItemResponse, error) {
actor := auth.MustActor(ctx)
brandID, err := requireCurrentBrandID(ctx)
if err != nil {
return nil, err
}
var sourceType string
var status string
err := s.pool.QueryRow(ctx, `
err = s.pool.QueryRow(ctx, `
SELECT source_type, status
FROM knowledge_items
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
`, itemID, actor.TenantID).Scan(&sourceType, &status)
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
`, itemID, actor.TenantID, brandID).Scan(&sourceType, &status)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40450, "knowledge_item_not_found", "knowledge item not found")
@@ -793,9 +842,10 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
updated_at = NOW()
WHERE id = $1
AND tenant_id = $2
AND brand_id = $3
AND deleted_at IS NULL
AND status = 'failed'
`, itemID, actor.TenantID)
`, itemID, actor.TenantID, brandID)
if err != nil {
return nil, response.ErrInternal(50055, "knowledge_item_retry_failed", "failed to reset knowledge item")
}
@@ -816,7 +866,7 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
return nil, response.ErrServiceUnavailable(50356, "knowledge_queue_unavailable", "knowledge parse queue is busy, please retry")
}
items, err := s.listKnowledgeItemsByIDs(ctx, actor.TenantID, []int64{itemID})
items, err := s.listKnowledgeItemsByIDs(ctx, actor.TenantID, brandID, []int64{itemID})
if err != nil {
return nil, err
}
@@ -829,6 +879,7 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
func (s *KnowledgeService) ResolveContext(
ctx context.Context,
tenantID int64,
brandID int64,
groupIDs []int64,
query string,
) (*KnowledgeContext, error) {
@@ -871,7 +922,7 @@ func (s *KnowledgeService) ResolveContext(
return nil, response.ErrServiceUnavailable(50352, "qdrant_unavailable", err.Error())
}
searchGroupIDs, groupNames, err := s.resolveKnowledgeSearchScope(ctx, tenantID, requestedGroupIDs)
searchGroupIDs, groupNames, err := s.resolveKnowledgeSearchScope(ctx, tenantID, brandID, requestedGroupIDs)
if err != nil {
return nil, err
}
@@ -883,7 +934,7 @@ func (s *KnowledgeService) ResolveContext(
GroupIDs: searchGroupIDs,
GroupNames: groupNames,
Snippets: []KnowledgeSnippet{},
PreciseFacts: s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, nil),
PreciseFacts: s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, nil),
}
vectors, err := s.provider.Embed(ctx, queries)
@@ -903,6 +954,7 @@ func (s *KnowledgeService) ResolveContext(
points, err := s.store.Search(ctx, retrieval.SearchRequest{
Vector: vector,
TenantID: tenantID,
BrandID: brandID,
GroupIDs: searchGroupIDs,
Limit: runtimeCfg.RecallLimit,
})
@@ -922,7 +974,7 @@ func (s *KnowledgeService) ResolveContext(
return baseContext, nil
}
candidates, err = s.filterActiveKnowledgeSnippets(ctx, tenantID, candidates)
candidates, err = s.filterActiveKnowledgeSnippets(ctx, tenantID, brandID, candidates)
if err != nil {
return nil, err
}
@@ -939,17 +991,17 @@ func (s *KnowledgeService) ResolveContext(
reranked, err := s.provider.Rerank(ctx, rerankQuery, documents, minInt(runtimeCfg.RerankTopN, len(documents)))
if err != nil {
selected := selectKnowledgeCandidateSnippets(candidates, runtimeCfg.RerankTopN)
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, brandID, selected)
baseContext.Snippets = selected
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, selected)
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, selected)
s.logKnowledgeResolve(baseContext, tenantID, rerankQuery, "rerank_failed_fallback", queryRewriteStage, queryRewriteModel, candidates, selected)
return baseContext, nil
}
selected := selectKnowledgeRerankedSnippets(candidates, reranked, runtimeCfg.RerankTopN)
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, brandID, selected)
baseContext.Snippets = selected
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, selected)
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, selected)
s.logKnowledgeResolve(baseContext, tenantID, rerankQuery, "resolved", queryRewriteStage, queryRewriteModel, candidates, selected)
return baseContext, nil
}
@@ -1341,6 +1393,7 @@ type knowledgePromptItemRecord struct {
func (s *KnowledgeService) enrichKnowledgePromptSnippets(
ctx context.Context,
tenantID int64,
brandID int64,
snippets []KnowledgeSnippet,
) []KnowledgeSnippet {
if len(snippets) == 0 {
@@ -1358,7 +1411,7 @@ func (s *KnowledgeService) enrichKnowledgePromptSnippets(
return snippets
}
items, err := s.loadKnowledgePromptItems(ctx, tenantID, itemIDs)
items, err := s.loadKnowledgePromptItems(ctx, tenantID, brandID, itemIDs)
if err != nil {
if s.logger != nil {
s.logger.Warn("load knowledge prompt items failed", zap.Error(err))
@@ -1395,6 +1448,7 @@ func (s *KnowledgeService) enrichKnowledgePromptSnippets(
func (s *KnowledgeService) loadKnowledgePromptItems(
ctx context.Context,
tenantID int64,
brandID int64,
itemIDs []int64,
) ([]knowledgePromptItemRecord, error) {
if len(itemIDs) == 0 {
@@ -1404,12 +1458,13 @@ func (s *KnowledgeService) loadKnowledgePromptItems(
rows, err := s.pool.Query(ctx, `
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.content_text, ki.markdown_content
FROM knowledge_items ki
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
WHERE ki.tenant_id = $1
AND ki.id = ANY($2::bigint[])
AND ki.brand_id = $2
AND ki.id = ANY($3::bigint[])
AND ki.status = 'completed'
AND ki.deleted_at IS NULL
`, tenantID, itemIDs)
`, tenantID, brandID, itemIDs)
if err != nil {
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to load knowledge prompt items")
}
@@ -1438,6 +1493,7 @@ func (s *KnowledgeService) loadKnowledgePromptItems(
func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
ctx context.Context,
tenantID int64,
brandID int64,
groupIDs []int64,
snippets []KnowledgeSnippet,
) []string {
@@ -1454,7 +1510,7 @@ func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
}
matchedItemIDs = normalizeInt64IDs(matchedItemIDs)
items, err := s.loadKnowledgePromptItemsByGroupIDs(ctx, tenantID, groupIDs, matchedItemIDs, 24)
items, err := s.loadKnowledgePromptItemsByGroupIDs(ctx, tenantID, brandID, groupIDs, matchedItemIDs, 24)
if err != nil {
if s.logger != nil {
s.logger.Warn("collect knowledge context precise facts failed", zap.Error(err))
@@ -1506,6 +1562,7 @@ func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
func (s *KnowledgeService) loadKnowledgePromptItemsByGroupIDs(
ctx context.Context,
tenantID int64,
brandID int64,
groupIDs []int64,
prioritizedItemIDs []int64,
limit int,
@@ -1524,31 +1581,34 @@ func (s *KnowledgeService) loadKnowledgePromptItemsByGroupIDs(
SELECT id
FROM knowledge_groups
WHERE tenant_id = $1
AND id = ANY($2::bigint[])
AND brand_id = $2
AND id = ANY($3::bigint[])
AND deleted_at IS NULL
UNION ALL
SELECT child.id
FROM knowledge_groups child
JOIN selected_groups parent ON child.parent_id = parent.id
WHERE child.tenant_id = $1
AND child.brand_id = $2
AND child.deleted_at IS NULL
)
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.content_text, ki.markdown_content
FROM knowledge_items ki
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
WHERE ki.tenant_id = $1
AND ki.brand_id = $2
AND ki.status = 'completed'
AND ki.deleted_at IS NULL
AND ki.group_id IN (SELECT id FROM selected_groups)
ORDER BY
CASE
WHEN ki.id = ANY($3::bigint[]) THEN 0
WHEN ki.id = ANY($4::bigint[]) THEN 0
ELSE 1
END,
ki.updated_at DESC,
ki.id DESC
LIMIT $4
`, tenantID, groupIDs, prioritizedItemIDs, limit)
LIMIT $5
`, tenantID, brandID, groupIDs, prioritizedItemIDs, limit)
if err != nil {
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to load knowledge prompt items by groups")
}
@@ -1859,9 +1919,10 @@ func (s *KnowledgeService) validateIngestionDependencies(sourceType string) erro
func (s *KnowledgeService) queueKnowledgeItem(
ctx context.Context,
tenantID int64,
brandID int64,
input knowledgeIngestInput,
) (*KnowledgeItemResponse, error) {
resolvedGroupID, err := s.resolveKnowledgeStorageGroupID(ctx, tenantID, input.GroupID)
resolvedGroupID, err := s.resolveKnowledgeStorageGroupID(ctx, tenantID, brandID, input.GroupID)
if err != nil {
return nil, err
}
@@ -1880,7 +1941,7 @@ func (s *KnowledgeService) queueKnowledgeItem(
input.ContentText = &contentText
}
itemID, parseTaskID, err := s.createPendingKnowledgeItem(ctx, tenantID, input)
itemID, parseTaskID, err := s.createPendingKnowledgeItem(ctx, tenantID, brandID, input)
if err != nil {
return nil, err
}
@@ -1894,7 +1955,7 @@ func (s *KnowledgeService) queueKnowledgeItem(
return nil, response.ErrServiceUnavailable(50356, "knowledge_queue_unavailable", "knowledge parse queue is busy, please retry")
}
items, err := s.listKnowledgeItemsByIDs(ctx, tenantID, []int64{itemID})
items, err := s.listKnowledgeItemsByIDs(ctx, tenantID, brandID, []int64{itemID})
if err != nil {
return nil, err
}
@@ -2064,6 +2125,7 @@ func (s *KnowledgeService) executeParseJob(ctx context.Context, job knowledgePar
Vector: embeddings[index],
Payload: map[string]any{
"tenant_id": job.TenantID,
"brand_id": item.BrandID,
"group_id": item.GroupID,
"group_name": item.GroupName,
"item_id": item.ID,
@@ -2105,14 +2167,15 @@ func (s *KnowledgeService) loadKnowledgeItemForParse(
var contentText *string
var markdownContent *string
err := s.pool.QueryRow(ctx, `
SELECT ki.id, ki.tenant_id, ki.group_id, kg.name, ki.name, ki.source_type,
SELECT ki.id, ki.tenant_id, ki.brand_id, ki.group_id, kg.name, ki.name, ki.source_type,
ki.source_uri, ki.storage_key, ki.content_text, ki.markdown_content, ki.item_version
FROM knowledge_items ki
JOIN knowledge_groups kg ON kg.id = ki.group_id
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NULL
`, itemID, tenantID).Scan(
&record.ID,
&record.TenantID,
&record.BrandID,
&record.GroupID,
&record.GroupName,
&record.Name,
@@ -2166,6 +2229,7 @@ type knowledgeChunkMetaInput struct {
func (s *KnowledgeService) createPendingKnowledgeItem(
ctx context.Context,
tenantID int64,
brandID int64,
input knowledgeIngestInput,
) (int64, int64, error) {
tx, err := s.pool.Begin(ctx)
@@ -2177,12 +2241,12 @@ func (s *KnowledgeService) createPendingKnowledgeItem(
var itemID int64
err = tx.QueryRow(ctx, `
INSERT INTO knowledge_items (
tenant_id, group_id, source_type, name, source_uri, storage_key,
tenant_id, brand_id, group_id, source_type, name, source_uri, storage_key,
content_text, markdown_content, status, size_bytes, item_version
)
VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, 'pending', $8, 1)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NULL, 'pending', $9, 1)
RETURNING id
`, tenantID, input.GroupID, input.SourceType, input.Name, input.SourceURI, input.StorageKey, input.ContentText, input.SizeBytes).Scan(&itemID)
`, tenantID, brandID, input.GroupID, input.SourceType, input.Name, input.SourceURI, input.StorageKey, input.ContentText, input.SizeBytes).Scan(&itemID)
if err != nil {
return 0, 0, response.ErrConflict(40951, "knowledge_item_create_failed", "failed to create knowledge item")
}
@@ -2434,15 +2498,15 @@ func buildKnowledgeSearchText(input knowledgeSearchTextInput) string {
return strings.Join(metadata, "\n")
}
func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID, groupID int64) error {
func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID, brandID, groupID int64) error {
var exists bool
err := s.pool.QueryRow(ctx, `
SELECT EXISTS(
SELECT 1
FROM knowledge_groups
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
)
`, groupID, tenantID).Scan(&exists)
`, groupID, tenantID, brandID).Scan(&exists)
if err != nil {
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
}
@@ -2452,8 +2516,8 @@ func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID,
return nil
}
func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupID int64) error {
if err := s.ensureGroupAccessible(ctx, tenantID, groupID); err != nil {
func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, brandID, groupID int64) error {
if err := s.ensureGroupAccessible(ctx, tenantID, brandID, groupID); err != nil {
return err
}
@@ -2464,10 +2528,11 @@ func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupI
FROM knowledge_groups
WHERE id = $1
AND tenant_id = $2
AND brand_id = $3
AND deleted_at IS NULL
AND parent_id IS NULL
)
`, groupID, tenantID).Scan(&isRoot)
`, groupID, tenantID, brandID).Scan(&isRoot)
if err != nil {
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
}
@@ -2477,8 +2542,8 @@ func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupI
return nil
}
func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, groupID int64) error {
if err := s.ensureGroupAccessible(ctx, tenantID, groupID); err != nil {
func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, brandID, groupID int64) error {
if err := s.ensureGroupAccessible(ctx, tenantID, brandID, groupID); err != nil {
return err
}
@@ -2490,13 +2555,15 @@ func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, gr
JOIN knowledge_groups parent
ON parent.id = child.parent_id
AND parent.tenant_id = child.tenant_id
AND parent.brand_id = child.brand_id
AND parent.deleted_at IS NULL
WHERE child.id = $1
AND child.tenant_id = $2
AND child.brand_id = $3
AND child.deleted_at IS NULL
AND parent.parent_id IS NULL
)
`, groupID, tenantID).Scan(&storable)
`, groupID, tenantID, brandID).Scan(&storable)
if err != nil {
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
}
@@ -2506,14 +2573,14 @@ func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, gr
return nil
}
func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, tenantID, groupID int64) (int64, error) {
func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, tenantID, brandID, groupID int64) (int64, error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return 0, response.ErrInternal(50055, "knowledge_group_resolve_failed", "failed to begin knowledge group transaction")
}
defer func() { _ = tx.Rollback(ctx) }()
resolvedGroupID, err := s.resolveKnowledgeStorageGroupIDTx(ctx, tx, tenantID, groupID)
resolvedGroupID, err := s.resolveKnowledgeStorageGroupIDTx(ctx, tx, tenantID, brandID, groupID)
if err != nil {
return 0, err
}
@@ -2527,15 +2594,15 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, t
func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
ctx context.Context,
tx pgx.Tx,
tenantID, groupID int64,
tenantID, brandID, groupID int64,
) (int64, error) {
var parentID *int64
if err := tx.QueryRow(ctx, `
SELECT parent_id
FROM knowledge_groups
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
FOR UPDATE
`, groupID, tenantID).Scan(&parentID); err != nil {
`, groupID, tenantID, brandID).Scan(&parentID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return 0, response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
}
@@ -2547,8 +2614,8 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
if err := tx.QueryRow(ctx, `
SELECT parent_id
FROM knowledge_groups
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
`, *parentID, tenantID).Scan(&grandParentID); err != nil {
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
`, *parentID, tenantID, brandID).Scan(&grandParentID); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return 0, response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
}
@@ -2565,22 +2632,23 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
SELECT id
FROM knowledge_groups
WHERE tenant_id = $1
AND parent_id = $2
AND name = $3
AND brand_id = $2
AND parent_id = $3
AND name = $4
AND deleted_at IS NULL
ORDER BY id
LIMIT 1
`, tenantID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err == nil {
`, tenantID, brandID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err == nil {
return childGroupID, nil
} else if !errors.Is(err, pgx.ErrNoRows) {
return 0, response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
}
if err := tx.QueryRow(ctx, `
INSERT INTO knowledge_groups (tenant_id, name, parent_id, sort_order)
VALUES ($1, $2, $3, 0)
INSERT INTO knowledge_groups (tenant_id, brand_id, name, parent_id, sort_order)
VALUES ($1, $2, $3, $4, 0)
RETURNING id
`, tenantID, defaultKnowledgeChildGroupName, groupID).Scan(&childGroupID); err == nil {
`, tenantID, brandID, defaultKnowledgeChildGroupName, groupID).Scan(&childGroupID); err == nil {
return childGroupID, nil
}
@@ -2588,12 +2656,13 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
SELECT id
FROM knowledge_groups
WHERE tenant_id = $1
AND parent_id = $2
AND name = $3
AND brand_id = $2
AND parent_id = $3
AND name = $4
AND deleted_at IS NULL
ORDER BY id
LIMIT 1
`, tenantID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err != nil {
`, tenantID, brandID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err != nil {
return 0, response.ErrConflict(40950, "knowledge_group_create_failed", "failed to create default knowledge group")
}
return childGroupID, nil
@@ -2602,22 +2671,22 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
func (s *KnowledgeService) listGroupSubtreeIDs(
ctx context.Context,
tx pgx.Tx,
tenantID, groupID int64,
tenantID, brandID, groupID int64,
) ([]int64, error) {
rows, err := tx.Query(ctx, `
WITH RECURSIVE selected_groups AS (
SELECT id
FROM knowledge_groups
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
UNION ALL
SELECT child.id
FROM knowledge_groups child
JOIN selected_groups parent ON child.parent_id = parent.id
WHERE child.tenant_id = $2 AND child.deleted_at IS NULL
WHERE child.tenant_id = $2 AND child.brand_id = $3 AND child.deleted_at IS NULL
)
SELECT id
FROM selected_groups
`, groupID, tenantID)
`, groupID, tenantID, brandID)
if err != nil {
return nil, response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to query knowledge groups")
}
@@ -2638,6 +2707,7 @@ func (s *KnowledgeService) listActiveKnowledgeItemIDs(
ctx context.Context,
tx pgx.Tx,
tenantID int64,
brandID int64,
groupIDs []int64,
) ([]int64, error) {
if len(groupIDs) == 0 {
@@ -2648,9 +2718,10 @@ func (s *KnowledgeService) listActiveKnowledgeItemIDs(
SELECT id
FROM knowledge_items
WHERE tenant_id = $1
AND brand_id = $2
AND deleted_at IS NULL
AND group_id = ANY($2::bigint[])
`, tenantID, groupIDs)
AND group_id = ANY($3::bigint[])
`, tenantID, brandID, groupIDs)
if err != nil {
return nil, response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to query knowledge items")
}
@@ -2714,6 +2785,7 @@ func (s *KnowledgeService) listKnowledgePointIDs(ctx context.Context, tenantID,
func (s *KnowledgeService) filterActiveKnowledgeSnippets(
ctx context.Context,
tenantID int64,
brandID int64,
candidates []KnowledgeSnippet,
) ([]KnowledgeSnippet, error) {
if len(candidates) == 0 {
@@ -2735,10 +2807,11 @@ func (s *KnowledgeService) filterActiveKnowledgeSnippets(
SELECT id
FROM knowledge_items
WHERE tenant_id = $1
AND id = ANY($2::bigint[])
AND brand_id = $2
AND id = ANY($3::bigint[])
AND status = 'completed'
AND deleted_at IS NULL
`, tenantID, itemIDs)
`, tenantID, brandID, itemIDs)
if err != nil {
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to filter deleted knowledge items")
}
@@ -2852,14 +2925,15 @@ func (s *KnowledgeService) loadKnowledgeItemForCleanup(
var contentText *string
var markdownContent *string
err := s.pool.QueryRow(ctx, `
SELECT ki.id, ki.tenant_id, ki.group_id, kg.name, ki.name, ki.source_type,
SELECT ki.id, ki.tenant_id, ki.brand_id, ki.group_id, kg.name, ki.name, ki.source_type,
ki.source_uri, ki.storage_key, ki.content_text, ki.markdown_content, ki.item_version
FROM knowledge_items ki
JOIN knowledge_groups kg ON kg.id = ki.group_id
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NOT NULL
`, itemID, tenantID).Scan(
&record.ID,
&record.TenantID,
&record.BrandID,
&record.GroupID,
&record.GroupName,
&record.Name,
@@ -2925,7 +2999,7 @@ func (s *KnowledgeService) clearKnowledgeCleanupError(ctx context.Context, tenan
return err
}
func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID int64, itemIDs []int64) ([]KnowledgeItemResponse, error) {
func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID, brandID int64, itemIDs []int64) ([]KnowledgeItemResponse, error) {
if len(itemIDs) == 0 {
return []KnowledgeItemResponse{}, nil
}
@@ -2933,10 +3007,10 @@ func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
ki.size_bytes, ki.error_message, ki.created_at, ki.updated_at
FROM knowledge_items ki
JOIN knowledge_groups kg ON kg.id = ki.group_id
WHERE ki.tenant_id = $1 AND ki.id = ANY($2::bigint[]) AND ki.deleted_at IS NULL
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
WHERE ki.tenant_id = $1 AND ki.brand_id = $2 AND ki.id = ANY($3::bigint[]) AND ki.deleted_at IS NULL
ORDER BY ki.created_at DESC, ki.id DESC
`, tenantID, itemIDs)
`, tenantID, brandID, itemIDs)
if err != nil {
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to list knowledge items")
}
@@ -2974,7 +3048,7 @@ func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID
return items, nil
}
func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tenantID int64, groupIDs []int64) ([]int64, []string, error) {
func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tenantID, brandID int64, groupIDs []int64) ([]int64, []string, error) {
groupIDs = normalizeInt64IDs(groupIDs)
if len(groupIDs) == 0 {
return []int64{}, []string{}, nil
@@ -2985,19 +3059,21 @@ func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tena
SELECT id, name, parent_id, sort_order, created_at
FROM knowledge_groups
WHERE tenant_id = $1
AND id = ANY($2::bigint[])
AND brand_id = $2
AND id = ANY($3::bigint[])
AND deleted_at IS NULL
UNION
SELECT child.id, child.name, child.parent_id, child.sort_order, child.created_at
FROM knowledge_groups child
JOIN selected_groups parent ON child.parent_id = parent.id
WHERE child.tenant_id = $1
AND child.brand_id = $2
AND child.deleted_at IS NULL
)
SELECT id, name
FROM selected_groups
ORDER BY sort_order, created_at, id
`, tenantID, groupIDs)
`, tenantID, brandID, groupIDs)
if err != nil {
return nil, nil, response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge groups")
}