Files
iqudo-top1/internal/run/knowledge.go
2026-08-20 21:58:04 +08:00

511 lines
18 KiB
Go

package run
import (
"encoding/json"
"fmt"
"regexp"
"sort"
"strings"
"git.iwork-ai.com/xdc/iqudo-top1/internal/model"
"gorm.io/gorm"
)
var knowledgePlaceholderPattern = regexp.MustCompile(`\{\{\s*(?:input|derived|form)\.([A-Za-z][A-Za-z0-9_]*)\s*\}\}`)
type NodeView struct {
NodeKey string `json:"node_key"`
Type string `json:"type"`
Title string `json:"title"`
Content string `json:"content"`
Config json.RawMessage `json:"config,omitempty"`
Fields []PublicFieldView `json:"fields,omitempty"`
Presentation *NodePresentation `json:"presentation,omitempty"`
Outputs []KnowledgeGroup `json:"outputs"`
Collection *KnowledgeCollectionView `json:"collection,omitempty"`
}
type PublicFieldView struct {
Key string `json:"key"`
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Options json.RawMessage `json:"options"`
Validation json.RawMessage `json:"validation"`
}
type KnowledgeGroup struct {
Key string `json:"key"`
Name string `json:"name"`
Type string `json:"type"`
Content json.RawMessage `json:"content"`
Relations map[string][]KnowledgeItemView `json:"relations"`
Suggested bool `json:"suggested,omitempty"`
}
type KnowledgeItemView struct {
Key string `json:"key"`
Name string `json:"name"`
Type string `json:"type"`
Content json.RawMessage `json:"content"`
}
type knowledgeNodeConfig struct {
KnowledgeSelector *knowledgeSelector `json:"knowledge_selector"`
KnowledgeCollection *knowledgeCollectionConfig `json:"knowledge_collection"`
}
type knowledgeCollectionConfig struct {
ContextFieldKeys []string `json:"context_field_keys"`
ContextTitle string `json:"context_title"`
ContextHint string `json:"context_hint"`
SelectionTitle string `json:"selection_title"`
SelectionHint string `json:"selection_hint"`
Steps []knowledgeCollectionStep `json:"steps"`
}
type knowledgeCollectionStep struct {
FieldKey string `json:"field_key"`
Name string `json:"name"`
Root bool `json:"root"`
CandidateScope string `json:"candidate_scope"`
KnowledgeTypes []string `json:"knowledge_types"`
FromField string `json:"from_field"`
RelationType string `json:"relation_type"`
Required bool `json:"required"`
Multiple bool `json:"multiple"`
}
type KnowledgeCollectionView struct {
ContextFields []PublicFieldView `json:"context_fields"`
ContextTitle string `json:"context_title"`
ContextHint string `json:"context_hint"`
SelectionTitle string `json:"selection_title"`
SelectionHint string `json:"selection_hint"`
Steps []KnowledgeCollectionStepView `json:"steps"`
}
type KnowledgeCollectionStepView struct {
FieldKey string `json:"field_key"`
Name string `json:"name"`
Required bool `json:"required"`
Multiple bool `json:"multiple"`
FromField string `json:"from_field,omitempty"`
Options []KnowledgeCollectionOption `json:"options"`
}
type KnowledgeCollectionOption struct {
Value string `json:"value"`
Label string `json:"label"`
Parents []string `json:"parents,omitempty"`
Suggested bool `json:"suggested,omitempty"`
}
type knowledgeSelector struct {
DerivedField string `json:"derived_field"`
AnswerField string `json:"answer_field"`
CandidateScope string `json:"candidate_scope"`
KnowledgeTypes []string `json:"knowledge_types"`
KnowledgeKeys []string `json:"knowledge_keys"`
RelationTypes []string `json:"relation_types"`
RelationLabels map[string]string `json:"relation_labels"`
}
func (h *Handler) nodeView(db *gorm.DB, node model.SOPNode, tenantID uint64, answers map[string]interface{}) (NodeView, error) {
view := NodeView{NodeKey: node.NodeKey, Type: node.Type, Title: node.Title, Content: node.Content, Config: json.RawMessage(node.Config)}
if node.Type == "start" {
presentation, err := loadStartPresentation(db, node, tenantID, answers)
if err != nil {
return view, err
}
view.Presentation = presentation
}
if node.Type == "question" || node.Type == "choice" || node.Type == "form" {
fields, err := loadPublicNodeFields(db, node, tenantID)
if err != nil {
return view, err
}
view.Fields = fields
}
if node.Type != "knowledge" {
return view, nil
}
var config knowledgeNodeConfig
if err := json.Unmarshal(node.Config, &config); err != nil {
return view, err
}
if config.KnowledgeSelector != nil {
outputs, err := loadKnowledgeOutputs(db, node, tenantID, answers, *config.KnowledgeSelector)
view.Outputs = outputs
if err != nil {
return view, err
}
if config.KnowledgeCollection != nil {
collection, err := loadKnowledgeCollection(db, node, tenantID, *config.KnowledgeCollection, outputs, answers)
view.Collection = &collection
return view, err
}
return view, nil
}
return view, nil
}
func loadKnowledgeCollection(db *gorm.DB, node model.SOPNode, tenantID uint64, config knowledgeCollectionConfig, roots []KnowledgeGroup, context map[string]interface{}) (KnowledgeCollectionView, error) {
fields, err := loadPublicFieldsByKeys(db, node, tenantID, config.ContextFieldKeys)
if err != nil {
return KnowledgeCollectionView{}, err
}
view := KnowledgeCollectionView{ContextFields: fields, ContextTitle: config.ContextTitle, ContextHint: config.ContextHint, SelectionTitle: config.SelectionTitle, SelectionHint: config.SelectionHint, Steps: make([]KnowledgeCollectionStepView, 0, len(config.Steps))}
childrenByParent := map[string]map[string][]string{}
allItems := make([]model.KnowledgeItem, 0)
if raw, ok := context["__knowledge_snapshot"].(json.RawMessage); ok && len(raw) > 0 {
snapshot, parseErr := parseKnowledgeSnapshot(raw)
if parseErr != nil {
return KnowledgeCollectionView{}, parseErr
}
byID := map[uint64]model.KnowledgeItem{}
for _, item := range snapshot.Items {
byID[item.ID] = item
if item.Status == "active" {
allItems = append(allItems, item)
}
}
for _, relation := range snapshot.Relations {
from, fromOK := byID[relation.FromKnowledgeID]
to, toOK := byID[relation.ToKnowledgeID]
if !fromOK || !toOK || from.Status != "active" || to.Status != "active" {
continue
}
if childrenByParent[from.Name] == nil {
childrenByParent[from.Name] = map[string][]string{}
}
childrenByParent[from.Name][relation.RelationType] = appendUnique(childrenByParent[from.Name][relation.RelationType], to.Name)
}
}
suggestedRoots := make(map[string]bool, len(roots))
rootOptions := make([]KnowledgeCollectionOption, 0, len(roots))
for _, root := range roots {
suggestedRoots[root.Name] = root.Suggested
rootOptions = append(rootOptions, KnowledgeCollectionOption{Value: root.Name, Label: root.Name, Suggested: root.Suggested})
}
for _, step := range config.Steps {
item := KnowledgeCollectionStepView{FieldKey: step.FieldKey, Name: step.Name, Required: step.Required, Multiple: step.Multiple, FromField: step.FromField, Options: []KnowledgeCollectionOption{}}
if step.Root {
item.Options = append(item.Options, rootOptions...)
if step.CandidateScope == "all" {
seen := map[string]bool{}
for _, option := range item.Options {
seen[option.Value] = true
}
for _, knowledgeItem := range allItems {
if seen[knowledgeItem.Name] || (len(step.KnowledgeTypes) > 0 && !containsString(step.KnowledgeTypes, knowledgeItem.Type)) {
continue
}
item.Options = append(item.Options, KnowledgeCollectionOption{Value: knowledgeItem.Name, Label: knowledgeItem.Name, Suggested: suggestedRoots[knowledgeItem.Name]})
}
sort.SliceStable(item.Options, func(i, j int) bool {
if item.Options[i].Suggested != item.Options[j].Suggested {
return item.Options[i].Suggested
}
return item.Options[i].Label < item.Options[j].Label
})
}
} else {
seen := map[string]*KnowledgeCollectionOption{}
var parentStep *KnowledgeCollectionStepView
for index := range view.Steps {
if view.Steps[index].FieldKey == step.FromField {
parentStep = &view.Steps[index]
break
}
}
if parentStep != nil {
for _, parent := range parentStep.Options {
for _, childName := range childrenByParent[parent.Value][step.RelationType] {
option := seen[childName]
if option == nil {
option = &KnowledgeCollectionOption{Value: childName, Label: childName}
seen[childName] = option
}
option.Parents = appendUnique(option.Parents, parent.Value)
}
}
}
for _, option := range seen {
item.Options = append(item.Options, *option)
}
sort.Slice(item.Options, func(i, j int) bool { return item.Options[i].Label < item.Options[j].Label })
}
view.Steps = append(view.Steps, item)
}
return view, nil
}
func loadPublicFieldsByKeys(db *gorm.DB, node model.SOPNode, tenantID uint64, keys []string) ([]PublicFieldView, error) {
if len(keys) == 0 {
return []PublicFieldView{}, nil
}
var fields []model.ScenarioField
if err := db.Table("scenario_fields sf").Select("sf.*").Joins("JOIN sops s ON s.scenario_id = sf.scenario_id").Where("s.id = ? AND sf.tenant_id = ? AND sf.field_key IN ?", node.SOPID, tenantID, keys).Find(&fields).Error; err != nil {
return nil, err
}
byKey := map[string]model.ScenarioField{}
for _, field := range fields {
byKey[field.FieldKey] = field
}
result := make([]PublicFieldView, 0, len(keys))
for _, key := range keys {
if field, ok := byKey[key]; ok {
result = append(result, PublicFieldView{Key: field.FieldKey, Name: field.FieldName, Type: field.FieldType, Required: field.Required, Options: json.RawMessage(field.Options), Validation: json.RawMessage(field.Validation)})
}
}
return result, nil
}
func appendUnique(values []string, value string) []string {
for _, existing := range values {
if existing == value {
return values
}
}
return append(values, value)
}
func loadPublicNodeFields(db *gorm.DB, node model.SOPNode, tenantID uint64) ([]PublicFieldView, error) {
var config answerNodeConfig
if err := json.Unmarshal(node.Config, &config); err != nil {
return nil, err
}
keys := config.FieldKeys
if config.FieldKey != "" {
keys = []string{config.FieldKey}
}
if len(keys) == 0 {
return nil, nil
}
var fields []model.ScenarioField
if err := db.Table("scenario_fields sf").Select("sf.*").Joins("JOIN sops s ON s.scenario_id = sf.scenario_id").Where("s.id = ? AND sf.tenant_id = ? AND sf.field_key IN ?", node.SOPID, tenantID, keys).Find(&fields).Error; err != nil {
return nil, err
}
byKey := make(map[string]model.ScenarioField, len(fields))
for _, field := range fields {
byKey[field.FieldKey] = field
}
views := make([]PublicFieldView, 0, len(keys))
for _, key := range keys {
field, ok := byKey[key]
if !ok {
continue
}
views = append(views, PublicFieldView{Key: field.FieldKey, Name: field.FieldName, Type: field.FieldType, Required: field.Required || config.Required || containsString(config.RequiredFieldKeys, key), Options: json.RawMessage(field.Options), Validation: json.RawMessage(field.Validation)})
}
return views, nil
}
func loadKnowledgeOutputs(db *gorm.DB, node model.SOPNode, tenantID uint64, context map[string]interface{}, selector knowledgeSelector) ([]KnowledgeGroup, error) {
var sopRow struct{ ScenarioID uint64 }
if err := db.Table("sop_nodes n").Select("s.scenario_id").Joins("JOIN sops s ON s.id = n.sop_id").Where("n.id = ? AND n.tenant_id = ?", node.ID, tenantID).Scan(&sopRow).Error; err != nil {
return nil, err
}
return loadKnowledgeOutputsForScenario(db, sopRow.ScenarioID, tenantID, context, selector)
}
func loadKnowledgeOutputsForScenario(db *gorm.DB, scenarioID, tenantID uint64, context map[string]interface{}, selector knowledgeSelector) ([]KnowledgeGroup, error) {
keys := append([]string{}, selector.KnowledgeKeys...)
if selector.DerivedField != "" {
value, _ := lookupContextValue(context, "derived."+selector.DerivedField)
keys = append(keys, stringSlice(value)...)
}
if selector.AnswerField != "" {
value, _ := lookupContextValue(context, "form."+selector.AnswerField)
keys = append(keys, stringSlice(value)...)
}
if len(keys) == 0 && selector.CandidateScope != "all" {
return []KnowledgeGroup{}, nil
}
keySet := make(map[string]bool, len(keys))
for _, key := range keys {
keySet[key] = true
}
items := make([]model.KnowledgeItem, 0)
relations := make([]model.KnowledgeRelation, 0)
snapshotTargets := map[uint64]model.KnowledgeItem{}
if raw, ok := context["__knowledge_snapshot"].(json.RawMessage); ok && len(raw) > 0 {
snapshot, err := parseKnowledgeSnapshot(raw)
if err != nil {
return nil, err
}
for _, item := range snapshot.Items {
snapshotTargets[item.ID] = item
if item.Status != "active" {
continue
}
if selector.CandidateScope != "all" && len(keys) > 0 && !knowledgeCandidateMatches(keySet, item) {
continue
}
if len(selector.KnowledgeTypes) > 0 && !containsString(selector.KnowledgeTypes, item.Type) {
continue
}
items = append(items, item)
}
for _, relation := range snapshot.Relations {
if len(selector.RelationTypes) > 0 && !containsString(selector.RelationTypes, relation.RelationType) {
continue
}
relations = append(relations, relation)
}
} else {
query := db.Where("tenant_id = ? AND scenario_id = ? AND status = ?", tenantID, scenarioID, "active")
if selector.CandidateScope != "all" && len(keys) > 0 {
query = query.Where("item_key IN ? OR name IN ?", keys, keys)
}
if len(selector.KnowledgeTypes) > 0 {
query = query.Where("type IN ?", selector.KnowledgeTypes)
}
if err := query.Order("sort_order, id").Find(&items).Error; err != nil {
return nil, err
}
}
ids := make([]uint64, 0, len(items))
for _, item := range items {
if selector.CandidateScope == "all" || len(keySet) == 0 || knowledgeCandidateMatches(keySet, item) {
ids = append(ids, item.ID)
}
}
if len(relations) == 0 && len(ids) > 0 {
rq := db.Where("tenant_id = ? AND scenario_id = ? AND from_knowledge_id IN ?", tenantID, scenarioID, ids)
if len(selector.RelationTypes) > 0 {
rq = rq.Where("relation_type IN ?", selector.RelationTypes)
}
if err := rq.Order("sort_order,id").Find(&relations).Error; err != nil {
return nil, err
}
}
targetIDs := make([]uint64, 0, len(relations))
for _, rel := range relations {
targetIDs = append(targetIDs, rel.ToKnowledgeID)
}
targets := make([]model.KnowledgeItem, 0)
if len(snapshotTargets) > 0 {
for _, id := range targetIDs {
if target, ok := snapshotTargets[id]; ok && target.Status == "active" {
targets = append(targets, target)
}
}
} else if len(targetIDs) > 0 {
if err := db.Where("tenant_id = ? AND id IN ? AND status = ?", tenantID, targetIDs, "active").Find(&targets).Error; err != nil {
return nil, err
}
}
targetMap := map[uint64]model.KnowledgeItem{}
for _, target := range targets {
targetMap[target.ID] = target
}
relationMap := map[uint64]map[string][]KnowledgeItemView{}
for _, rel := range relations {
matched, err := matchCondition(json.RawMessage(rel.Condition), context)
if err != nil {
return nil, fmt.Errorf("知识关系 %d 条件不正确: %w", rel.ID, err)
}
if !matched {
continue
}
target, ok := targetMap[rel.ToKnowledgeID]
if !ok {
continue
}
if relationMap[rel.FromKnowledgeID] == nil {
relationMap[rel.FromKnowledgeID] = map[string][]KnowledgeItemView{}
}
relationMap[rel.FromKnowledgeID][rel.RelationType] = append(relationMap[rel.FromKnowledgeID][rel.RelationType], KnowledgeItemView{Key: target.ItemKey, Name: target.Name, Type: target.Type, Content: renderKnowledgeContent(target.Content, context)})
}
outputs := make([]KnowledgeGroup, 0, len(items))
for _, item := range items {
outputs = append(outputs, KnowledgeGroup{Key: item.ItemKey, Name: item.Name, Type: item.Type, Content: renderKnowledgeContent(item.Content, context), Relations: relationMap[item.ID], Suggested: knowledgeCandidateMatches(keySet, item)})
}
return outputs, nil
}
func knowledgeCandidateMatches(candidates map[string]bool, item model.KnowledgeItem) bool {
return candidates[item.ItemKey] || candidates[item.Name]
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}
func renderKnowledgeContent(raw []byte, context map[string]interface{}) json.RawMessage {
var value interface{}
if json.Unmarshal(raw, &value) != nil {
return json.RawMessage(raw)
}
value = renderKnowledgeValue(value, context)
rendered, _ := json.Marshal(value)
return rendered
}
func renderKnowledgeValue(value interface{}, context map[string]interface{}) interface{} {
switch typed := value.(type) {
case string:
return knowledgePlaceholderPattern.ReplaceAllStringFunc(typed, func(token string) string {
match := knowledgePlaceholderPattern.FindStringSubmatch(token)
if len(match) != 2 {
return token
}
resolved, ok := lookupContextValue(context, tokenNamespaceKey(token, match[1]))
if !ok || resolved == nil {
return "未提供"
}
if values, ok := resolved.([]interface{}); ok {
parts := make([]string, 0, len(values))
for _, item := range values {
parts = append(parts, fmt.Sprint(item))
}
return strings.Join(parts, "、")
}
return fmt.Sprint(resolved)
})
case []interface{}:
for index, item := range typed {
typed[index] = renderKnowledgeValue(item, context)
}
return typed
case map[string]interface{}:
for key, item := range typed {
typed[key] = renderKnowledgeValue(item, context)
}
return typed
default:
return value
}
}
func tokenNamespaceKey(token, key string) string {
trimmed := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(token, "{{"), "}}"))
if strings.Contains(trimmed, ".") {
return trimmed
}
return key
}
func stringSlice(value interface{}) []string {
result := []string{}
switch values := value.(type) {
case []interface{}:
for _, item := range values {
if text, ok := item.(string); ok {
result = append(result, text)
}
}
case []string:
return values
case string:
return []string{values}
}
return result
}