feat: Enhance Kol Generation Service with web search and knowledge group support
- Added `EnableWebSearch` and `KnowledgeGroupIDs` fields to `KolGenerationSubmitRequest`. - Updated `Submit` method in `KolGenerationService` to handle new request fields. - Integrated web search and knowledge group validation based on card configuration. - Introduced caching mechanisms in `KolPackageService`, `KolPromptService`, and `KolSubscriptionAdminService` to improve performance. - Implemented knowledge context resolution in `KolGenerationWorker` to enrich prompts with relevant knowledge snippets. - Added utility functions for handling card configuration in both backend and frontend. - Created tests for knowledge extraction and rendering to ensure accuracy and reliability.
This commit is contained in:
@@ -3,6 +3,8 @@ package generate
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -23,6 +25,7 @@ const kolGenerationTaskType = "kol"
|
||||
type KolGenerationWorker struct {
|
||||
pool *pgxpool.Pool
|
||||
llm llm.Client
|
||||
knowledge *tenantapp.KnowledgeService
|
||||
logger *zap.Logger
|
||||
articleTimeout time.Duration
|
||||
maxOutputTokens int64
|
||||
@@ -32,6 +35,7 @@ type KolGenerationWorker struct {
|
||||
func NewKolGenerationWorker(
|
||||
pool *pgxpool.Pool,
|
||||
llmClient llm.Client,
|
||||
knowledgeSvc *tenantapp.KnowledgeService,
|
||||
logger *zap.Logger,
|
||||
cfg config.GenerationConfig,
|
||||
llmMaxOutputTokens int64,
|
||||
@@ -44,6 +48,7 @@ func NewKolGenerationWorker(
|
||||
return &KolGenerationWorker{
|
||||
pool: pool,
|
||||
llm: llmClient,
|
||||
knowledge: knowledgeSvc,
|
||||
logger: logger,
|
||||
articleTimeout: articleTimeout,
|
||||
maxOutputTokens: llmMaxOutputTokens,
|
||||
@@ -84,11 +89,41 @@ func (w *KolGenerationWorker) Process(ctx context.Context, task tenantapp.Claime
|
||||
return
|
||||
}
|
||||
|
||||
result, err := w.llm.Generate(ctx, llm.GenerateRequest{
|
||||
Prompt: renderedPrompt,
|
||||
knowledgePrompt := ""
|
||||
if knowledgeGroupIDs := kolKnowledgeGroupIDs(task.InputParams["knowledge_group_ids"]); len(knowledgeGroupIDs) > 0 {
|
||||
if w.knowledge == nil {
|
||||
w.HandleTaskFailure(ctx, task, "knowledge_resolve", fmt.Errorf("knowledge service is not configured"))
|
||||
return
|
||||
}
|
||||
|
||||
knowledgeContext, err := w.knowledge.ResolveContext(
|
||||
ctx,
|
||||
task.TenantID,
|
||||
knowledgeGroupIDs,
|
||||
buildKolKnowledgeQuery(task.InputParams),
|
||||
)
|
||||
if err != nil {
|
||||
w.HandleTaskFailure(ctx, task, "knowledge_resolve", err)
|
||||
return
|
||||
}
|
||||
knowledgePrompt = w.knowledge.RenderPromptSection(knowledgeContext)
|
||||
}
|
||||
|
||||
prompt := renderedPrompt
|
||||
if strings.TrimSpace(knowledgePrompt) != "" {
|
||||
prompt = strings.TrimSpace(renderedPrompt + "\n\n" + knowledgePrompt)
|
||||
}
|
||||
|
||||
generateReq := llm.GenerateRequest{
|
||||
Prompt: prompt,
|
||||
Timeout: w.articleTimeout,
|
||||
MaxOutputTokens: w.maxOutputTokens,
|
||||
}, nil)
|
||||
}
|
||||
if kolBoolInput(task.InputParams, "enable_web_search") {
|
||||
generateReq.WebSearch = &llm.WebSearchOptions{Enabled: true}
|
||||
}
|
||||
|
||||
result, err := w.llm.Generate(ctx, generateReq, nil)
|
||||
if err != nil {
|
||||
w.HandleTaskFailure(ctx, task, "llm_generate", err)
|
||||
return
|
||||
@@ -216,6 +251,137 @@ func kolStringInput(input map[string]interface{}, key string) string {
|
||||
return text
|
||||
}
|
||||
|
||||
func kolBoolInput(input map[string]interface{}, key string) bool {
|
||||
if input == nil {
|
||||
return false
|
||||
}
|
||||
value, ok := input[key]
|
||||
if !ok || value == nil {
|
||||
return false
|
||||
}
|
||||
enabled, ok := value.(bool)
|
||||
return ok && enabled
|
||||
}
|
||||
|
||||
func kolKnowledgeGroupIDs(value interface{}) []int64 {
|
||||
switch typed := value.(type) {
|
||||
case []int64:
|
||||
return dedupeKolIDs(typed)
|
||||
case []interface{}:
|
||||
ids := make([]int64, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
switch raw := item.(type) {
|
||||
case int64:
|
||||
ids = append(ids, raw)
|
||||
case int32:
|
||||
ids = append(ids, int64(raw))
|
||||
case int:
|
||||
ids = append(ids, int64(raw))
|
||||
case float64:
|
||||
ids = append(ids, int64(raw))
|
||||
case float32:
|
||||
ids = append(ids, int64(raw))
|
||||
}
|
||||
}
|
||||
return dedupeKolIDs(ids)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func dedupeKolIDs(ids []int64) []int64 {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
result := make([]int64, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[id]; exists {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i] < result[j]
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func buildKolKnowledgeQuery(input map[string]interface{}) string {
|
||||
parts := make([]string, 0, 8)
|
||||
if promptName := strings.TrimSpace(kolStringInput(input, "prompt_name")); promptName != "" {
|
||||
parts = append(parts, promptName)
|
||||
}
|
||||
if platformHint := strings.TrimSpace(kolStringInput(input, "platform_hint")); platformHint != "" {
|
||||
parts = append(parts, platformHint)
|
||||
}
|
||||
|
||||
variables, _ := input["variables"].(map[string]interface{})
|
||||
if len(variables) > 0 {
|
||||
keys := make([]string, 0, len(variables))
|
||||
for key := range variables {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
valueText := strings.TrimSpace(kolValueString(variables[key]))
|
||||
if valueText == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s: %s", key, valueText))
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) == 0 {
|
||||
if renderedPrompt := strings.TrimSpace(kolStringInput(input, "rendered_prompt")); renderedPrompt != "" {
|
||||
parts = append(parts, renderedPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
return kolLimitText(strings.Join(parts, "\n"), 2000)
|
||||
}
|
||||
|
||||
func kolValueString(value interface{}) string {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
return typed
|
||||
case bool:
|
||||
if typed {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case []interface{}:
|
||||
items := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
text := strings.TrimSpace(kolValueString(item))
|
||||
if text != "" {
|
||||
items = append(items, text)
|
||||
}
|
||||
}
|
||||
return strings.Join(items, ", ")
|
||||
default:
|
||||
return fmt.Sprintf("%v", typed)
|
||||
}
|
||||
}
|
||||
|
||||
func kolLimitText(value string, maxRunes int) string {
|
||||
text := strings.TrimSpace(value)
|
||||
if text == "" || maxRunes <= 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
runes := []rune(text)
|
||||
if len(runes) <= maxRunes {
|
||||
return text
|
||||
}
|
||||
return strings.TrimSpace(string(runes[:maxRunes]))
|
||||
}
|
||||
|
||||
func kolResolveArticleTitle(markdown string) string {
|
||||
for _, line := range strings.Split(markdown, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
Reference in New Issue
Block a user