feat: add knowledge management functionality with CRUD operations and database schema
- Implemented KnowledgeHandler for managing knowledge groups and items, including listing, creating, updating, and deleting operations. - Added database migration scripts to create necessary tables for knowledge management, including knowledge_groups, knowledge_items, knowledge_parse_tasks, and knowledge_chunks_meta. - Introduced prompt_rule_knowledge_groups table to associate prompt rules with knowledge groups.
This commit is contained in:
@@ -15,12 +15,14 @@ import (
|
||||
"github.com/geo-platform/tenant-api/internal/shared/llm"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/stream"
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/prompts"
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
||||
)
|
||||
|
||||
type PromptRuleGenerationService struct {
|
||||
pool *pgxpool.Pool
|
||||
llm llm.Client
|
||||
knowledge *KnowledgeService
|
||||
streams *stream.GenerationHub
|
||||
streamEnabled bool
|
||||
articleTimeout time.Duration
|
||||
@@ -53,13 +55,15 @@ type GenerateFromRuleResponse struct {
|
||||
}
|
||||
|
||||
type promptRuleGenerationRecord struct {
|
||||
ID int64
|
||||
Name string
|
||||
PromptContent string
|
||||
Scene *string
|
||||
DefaultTone *string
|
||||
DefaultWordCount *int
|
||||
Status string
|
||||
ID int64
|
||||
Name string
|
||||
PromptContent string
|
||||
Scene *string
|
||||
DefaultTone *string
|
||||
DefaultWordCount *int
|
||||
KnowledgeGroupIDs []int64
|
||||
KnowledgeGroups []PromptRuleKnowledgeGroup
|
||||
Status string
|
||||
}
|
||||
|
||||
const promptRuleGeneratingTitlePlaceholder = "标题生成中"
|
||||
@@ -67,6 +71,7 @@ const promptRuleGeneratingTitlePlaceholder = "标题生成中"
|
||||
func NewPromptRuleGenerationService(
|
||||
pool *pgxpool.Pool,
|
||||
llmClient llm.Client,
|
||||
knowledge *KnowledgeService,
|
||||
streams *stream.GenerationHub,
|
||||
cfg config.GenerationConfig,
|
||||
llmMaxOutputTokens int64,
|
||||
@@ -89,6 +94,7 @@ func NewPromptRuleGenerationService(
|
||||
svc := &PromptRuleGenerationService{
|
||||
pool: pool,
|
||||
llm: llmClient,
|
||||
knowledge: knowledge,
|
||||
streams: streams,
|
||||
streamEnabled: cfg.StreamEnabled,
|
||||
articleTimeout: articleTimeout,
|
||||
@@ -153,6 +159,26 @@ func (s *PromptRuleGenerationService) GenerateFromRule(ctx context.Context, req
|
||||
if req.BrandID != nil {
|
||||
inputParams["brand_id"] = *req.BrandID
|
||||
}
|
||||
if len(rule.KnowledgeGroupIDs) > 0 {
|
||||
inputParams["knowledge_group_ids"] = rule.KnowledgeGroupIDs
|
||||
}
|
||||
|
||||
knowledgePrompt := ""
|
||||
if len(rule.KnowledgeGroupIDs) > 0 {
|
||||
if s.knowledge == nil {
|
||||
return nil, response.ErrServiceUnavailable(50352, "knowledge_unavailable", "knowledge service is not configured")
|
||||
}
|
||||
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
actor.TenantID,
|
||||
rule.KnowledgeGroupIDs,
|
||||
buildPromptRuleKnowledgeQuery(rule, inputParams),
|
||||
)
|
||||
if resolveErr != nil {
|
||||
return nil, resolveErr
|
||||
}
|
||||
knowledgePrompt = s.knowledge.RenderPromptSection(knowledgeContext)
|
||||
}
|
||||
|
||||
inputJSON, _ := json.Marshal(inputParams)
|
||||
wizardStateJSON, _ := mergeArticleWizardState(nil, promptRuleGeneratingTitlePlaceholder, platformIDs)
|
||||
@@ -233,7 +259,7 @@ func (s *PromptRuleGenerationService) GenerateFromRule(ctx context.Context, req
|
||||
ArticleID: articleID,
|
||||
TaskID: taskID,
|
||||
ReservationID: reservationID,
|
||||
Prompt: buildPromptRuleGenerationPrompt(rule, inputParams),
|
||||
Prompt: buildPromptRuleGenerationPrompt(rule, inputParams, knowledgePrompt),
|
||||
InputParams: inputParams,
|
||||
InitialTitle: promptRuleGeneratingTitlePlaceholder,
|
||||
WebSearch: llm.WebSearchOptions{
|
||||
@@ -271,6 +297,28 @@ func (s *PromptRuleGenerationService) loadPromptRule(ctx context.Context, tenant
|
||||
if err != nil {
|
||||
return nil, response.ErrNotFound(40431, "rule_not_found", "prompt rule not found")
|
||||
}
|
||||
knowledgeRows, err := s.pool.Query(ctx, `
|
||||
SELECT kg.id, kg.name
|
||||
FROM prompt_rule_knowledge_groups prkg
|
||||
JOIN knowledge_groups kg ON kg.id = prkg.knowledge_group_id
|
||||
WHERE prkg.prompt_rule_id = $1
|
||||
AND kg.tenant_id = $2
|
||||
AND kg.deleted_at IS NULL
|
||||
ORDER BY kg.sort_order, kg.created_at, kg.id
|
||||
`, promptRuleID, tenantID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50010, "knowledge_group_query_failed", "failed to query knowledge groups")
|
||||
}
|
||||
defer knowledgeRows.Close()
|
||||
|
||||
for knowledgeRows.Next() {
|
||||
var group PromptRuleKnowledgeGroup
|
||||
if err := knowledgeRows.Scan(&group.ID, &group.Name); err != nil {
|
||||
return nil, response.ErrInternal(50010, "knowledge_group_query_failed", err.Error())
|
||||
}
|
||||
record.KnowledgeGroups = append(record.KnowledgeGroups, group)
|
||||
record.KnowledgeGroupIDs = append(record.KnowledgeGroupIDs, group.ID)
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
@@ -447,34 +495,60 @@ func (s *PromptRuleGenerationService) failGeneration(ctx context.Context, job pr
|
||||
}
|
||||
}
|
||||
|
||||
func buildPromptRuleGenerationPrompt(rule *promptRuleGenerationRecord, params map[string]interface{}) string {
|
||||
func buildPromptRuleGenerationPrompt(rule *promptRuleGenerationRecord, params map[string]interface{}, knowledgePrompt string) string {
|
||||
sections := []string{strings.TrimSpace(rule.PromptContent)}
|
||||
|
||||
var supplements []string
|
||||
if taskName := strings.TrimSpace(extractString(params, "task_name")); taskName != "" {
|
||||
supplements = append(supplements, "任务名称:"+taskName)
|
||||
supplements = append(supplements, prompts.PromptRuleTaskNameSupplement(taskName))
|
||||
}
|
||||
if rule.Scene != nil && strings.TrimSpace(*rule.Scene) != "" {
|
||||
supplements = append(supplements, "适用场景:"+strings.TrimSpace(*rule.Scene))
|
||||
supplements = append(supplements, prompts.PromptRuleSceneSupplement(strings.TrimSpace(*rule.Scene)))
|
||||
}
|
||||
if rule.DefaultTone != nil && strings.TrimSpace(*rule.DefaultTone) != "" {
|
||||
supplements = append(supplements, "默认语气:"+strings.TrimSpace(*rule.DefaultTone))
|
||||
supplements = append(supplements, prompts.PromptRuleToneSupplement(strings.TrimSpace(*rule.DefaultTone)))
|
||||
}
|
||||
if rule.DefaultWordCount != nil && *rule.DefaultWordCount > 0 {
|
||||
supplements = append(supplements, "建议字数:"+strconv.Itoa(*rule.DefaultWordCount)+" 字左右")
|
||||
supplements = append(supplements, prompts.PromptRuleWordCountSupplement(*rule.DefaultWordCount))
|
||||
}
|
||||
if target := strings.TrimSpace(extractString(params, "target_platform")); target != "" {
|
||||
supplements = append(supplements, "目标发布平台:"+strings.ReplaceAll(target, ",", "、"))
|
||||
supplements = append(supplements, prompts.PromptRuleTargetPlatformSupplement(target))
|
||||
}
|
||||
|
||||
if len(supplements) > 0 {
|
||||
sections = append(sections, "补充要求:\n- "+strings.Join(supplements, "\n- "))
|
||||
sections = append(sections, prompts.PromptRuleSupplementSection(supplements))
|
||||
}
|
||||
|
||||
sections = append(sections, "输出要求:\n1. 直接输出完整可发布的中文 Markdown 文章。\n2. 标题使用一级标题,并由 AI 根据正文内容自行生成,不要直接复用任务名称。\n3. 不要输出你的思考过程、解释或致歉。")
|
||||
if strings.TrimSpace(knowledgePrompt) != "" {
|
||||
sections = append(sections, knowledgePrompt)
|
||||
}
|
||||
|
||||
sections = append(sections, prompts.PromptRuleOutputRequirementsSection())
|
||||
return strings.Join(sections, "\n\n")
|
||||
}
|
||||
|
||||
func buildPromptRuleKnowledgeQuery(rule *promptRuleGenerationRecord, params map[string]interface{}) string {
|
||||
parts := make([]string, 0, 6)
|
||||
if rule != nil {
|
||||
if text := strings.TrimSpace(rule.Name); text != "" {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
if rule.Scene != nil && strings.TrimSpace(*rule.Scene) != "" {
|
||||
parts = append(parts, strings.TrimSpace(*rule.Scene))
|
||||
}
|
||||
if text := strings.TrimSpace(rule.PromptContent); text != "" {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
}
|
||||
if taskName := strings.TrimSpace(extractString(params, "task_name")); taskName != "" {
|
||||
parts = append(parts, taskName)
|
||||
}
|
||||
if target := strings.TrimSpace(extractString(params, "target_platform")); target != "" {
|
||||
parts = append(parts, target)
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
func extractGenerateCount(extra map[string]interface{}) int {
|
||||
value := extractInt(extra, "generate_count")
|
||||
if value <= 0 {
|
||||
|
||||
Reference in New Issue
Block a user