207 lines
6.2 KiB
Go
207 lines
6.2 KiB
Go
|
|
package usecase
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
app "haixun-backend/internal/library/errors"
|
||
|
|
"haixun-backend/internal/library/errors/code"
|
||
|
|
libkg "haixun-backend/internal/library/knowledge"
|
||
|
|
libmongo "haixun-backend/internal/library/mongo"
|
||
|
|
"haixun-backend/internal/model/knowledge_graph/domain/entity"
|
||
|
|
domrepo "haixun-backend/internal/model/knowledge_graph/domain/repository"
|
||
|
|
domusecase "haixun-backend/internal/model/knowledge_graph/domain/usecase"
|
||
|
|
)
|
||
|
|
|
||
|
|
type knowledgeGraphUseCase struct {
|
||
|
|
repo domrepo.Repository
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewUseCase(repo domrepo.Repository) domusecase.UseCase {
|
||
|
|
return &knowledgeGraphUseCase{repo: repo}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *knowledgeGraphUseCase) Get(ctx context.Context, tenantID, ownerUID, brandID string) (*domusecase.GraphSummary, error) {
|
||
|
|
if err := requireScope(tenantID, ownerUID, brandID, ""); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
item, err := u.repo.FindByBrand(ctx, tenantID, ownerUID, brandID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
summary := toSummary(item)
|
||
|
|
return &summary, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *knowledgeGraphUseCase) GetByTopic(ctx context.Context, tenantID, ownerUID, topicID, brandID string) (*domusecase.GraphSummary, error) {
|
||
|
|
if err := requireScope(tenantID, ownerUID, "", topicID); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
item, err := u.repo.FindByTopic(ctx, tenantID, ownerUID, topicID)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if item == nil {
|
||
|
|
brandID = strings.TrimSpace(brandID)
|
||
|
|
if brandID != "" {
|
||
|
|
legacy, legacyErr := u.repo.FindByBrand(ctx, tenantID, ownerUID, brandID)
|
||
|
|
if legacyErr != nil {
|
||
|
|
return nil, legacyErr
|
||
|
|
}
|
||
|
|
if legacy != nil {
|
||
|
|
legacyTopicID := strings.TrimSpace(legacy.TopicID)
|
||
|
|
if legacyTopicID == "" || legacyTopicID == topicID {
|
||
|
|
if legacyTopicID == "" {
|
||
|
|
if err := u.repo.AttachTopicID(ctx, tenantID, ownerUID, brandID, topicID); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
legacy.TopicID = topicID
|
||
|
|
}
|
||
|
|
item = legacy
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
summary := toSummary(item)
|
||
|
|
return &summary, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *knowledgeGraphUseCase) Upsert(ctx context.Context, req domusecase.UpsertRequest) (*domusecase.GraphSummary, error) {
|
||
|
|
topicID := strings.TrimSpace(req.TopicID)
|
||
|
|
brandID := strings.TrimSpace(req.BrandID)
|
||
|
|
if err := requireScope(req.TenantID, req.OwnerUID, brandID, topicID); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
seed := strings.TrimSpace(req.Seed)
|
||
|
|
if seed == "" {
|
||
|
|
return nil, app.For(code.Brand).InputMissingRequired("seed is required")
|
||
|
|
}
|
||
|
|
graph := &entity.Graph{
|
||
|
|
TenantID: req.TenantID,
|
||
|
|
OwnerUID: req.OwnerUID,
|
||
|
|
BrandID: brandID,
|
||
|
|
TopicID: topicID,
|
||
|
|
Seed: seed,
|
||
|
|
Nodes: req.Nodes,
|
||
|
|
Edges: req.Edges,
|
||
|
|
BraveSources: req.BraveSources,
|
||
|
|
ExpandStrategy: req.ExpandStrategy,
|
||
|
|
PainTagCount: req.PainTagCount,
|
||
|
|
GeneratedAt: req.GeneratedAt,
|
||
|
|
}
|
||
|
|
var (
|
||
|
|
item *entity.Graph
|
||
|
|
err error
|
||
|
|
)
|
||
|
|
if topicID != "" {
|
||
|
|
item, err = u.repo.UpsertByTopic(ctx, graph)
|
||
|
|
} else {
|
||
|
|
item, err = u.repo.UpsertByBrand(ctx, graph)
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
summary := toSummary(item)
|
||
|
|
return &summary, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *knowledgeGraphUseCase) UpdateNodes(ctx context.Context, req domusecase.UpdateNodesRequest) (*domusecase.GraphSummary, error) {
|
||
|
|
topicID := strings.TrimSpace(req.TopicID)
|
||
|
|
brandID := strings.TrimSpace(req.BrandID)
|
||
|
|
if err := requireScope(req.TenantID, req.OwnerUID, brandID, topicID); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if len(req.Updates) == 0 {
|
||
|
|
return nil, app.For(code.Brand).InputMissingRequired("updates is required")
|
||
|
|
}
|
||
|
|
var (
|
||
|
|
current *entity.Graph
|
||
|
|
err error
|
||
|
|
)
|
||
|
|
if topicID != "" {
|
||
|
|
current, err = u.repo.FindByTopic(ctx, req.TenantID, req.OwnerUID, topicID)
|
||
|
|
} else {
|
||
|
|
current, err = u.repo.FindByBrand(ctx, req.TenantID, req.OwnerUID, brandID)
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
changes := map[string]domusecase.NodeUpdate{}
|
||
|
|
for _, update := range req.Updates {
|
||
|
|
id := strings.TrimSpace(update.NodeID)
|
||
|
|
if id == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
changes[id] = update
|
||
|
|
}
|
||
|
|
nodes := make([]libkg.Node, len(current.Nodes))
|
||
|
|
copy(nodes, current.Nodes)
|
||
|
|
for i := range nodes {
|
||
|
|
update, ok := changes[nodes[i].ID]
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if update.SelectedForScan != nil {
|
||
|
|
nodes[i].SelectedForScan = *update.SelectedForScan
|
||
|
|
}
|
||
|
|
if update.RelevanceTagsSet {
|
||
|
|
nodes[i].PatrolRelevance = libkg.SanitizePatrolKeywordList(update.RelevanceTags)
|
||
|
|
}
|
||
|
|
if update.RecencyTagsSet {
|
||
|
|
nodes[i].PatrolRecency = libkg.SanitizePatrolKeywordList(update.RecencyTags)
|
||
|
|
}
|
||
|
|
if update.RelevanceTagsSet || update.RecencyTagsSet {
|
||
|
|
nodes[i].DerivedTags = libkg.DerivePatrolTagsForNode(nodes[i], libkg.PatrolTagInput{})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
painCount := libkg.CountPainTagCandidates(nodes)
|
||
|
|
var item *entity.Graph
|
||
|
|
if topicID != "" {
|
||
|
|
item, err = u.repo.UpdateNodesByTopic(ctx, req.TenantID, req.OwnerUID, topicID, nodes, painCount)
|
||
|
|
} else {
|
||
|
|
item, err = u.repo.UpdateNodes(ctx, req.TenantID, req.OwnerUID, brandID, nodes, painCount)
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
summary := toSummary(item)
|
||
|
|
return &summary, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (u *knowledgeGraphUseCase) AttachTopicID(ctx context.Context, tenantID, ownerUID, brandID, topicID string) error {
|
||
|
|
if err := requireScope(tenantID, ownerUID, brandID, topicID); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return u.repo.AttachTopicID(ctx, tenantID, ownerUID, brandID, topicID)
|
||
|
|
}
|
||
|
|
|
||
|
|
func requireScope(tenantID, ownerUID, brandID, topicID string) error {
|
||
|
|
if strings.TrimSpace(tenantID) == "" || strings.TrimSpace(ownerUID) == "" {
|
||
|
|
return app.For(code.Brand).InputMissingRequired("tenant_id and uid are required")
|
||
|
|
}
|
||
|
|
if strings.TrimSpace(topicID) == "" && strings.TrimSpace(brandID) == "" {
|
||
|
|
return app.For(code.Brand).InputMissingRequired("brand id or topic id is required")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func toSummary(item *entity.Graph) domusecase.GraphSummary {
|
||
|
|
if item == nil {
|
||
|
|
return domusecase.GraphSummary{}
|
||
|
|
}
|
||
|
|
return domusecase.GraphSummary{
|
||
|
|
ID: item.ID,
|
||
|
|
BrandID: libmongo.ResolveBrandID(item.BrandID, item.LegacyPersonaID),
|
||
|
|
TopicID: strings.TrimSpace(item.TopicID),
|
||
|
|
Seed: item.Seed,
|
||
|
|
Nodes: item.Nodes,
|
||
|
|
Edges: item.Edges,
|
||
|
|
BraveSources: item.BraveSources,
|
||
|
|
ExpandStrategy: item.ExpandStrategy,
|
||
|
|
PainTagCount: item.PainTagCount,
|
||
|
|
GeneratedAt: item.GeneratedAt,
|
||
|
|
CreateAt: item.CreateAt,
|
||
|
|
UpdateAt: item.UpdateAt,
|
||
|
|
}
|
||
|
|
}
|