Optimierter Workflow
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/local/glpi-neural-brain/internal/model"
|
||||
)
|
||||
|
||||
func clusterPendingArticleCandidates(values []*pendingArticleCandidate) [][]*pendingArticleCandidate {
|
||||
clusters := make([][]*pendingArticleCandidate, 0)
|
||||
for _, value := range values {
|
||||
if value == nil || len(value.Seeds) == 0 {
|
||||
continue
|
||||
}
|
||||
placed := false
|
||||
for i := range clusters {
|
||||
for _, existing := range clusters[i] {
|
||||
if articleCandidatesBelongTogether(existing, value) {
|
||||
clusters[i] = append(clusters[i], value)
|
||||
placed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if placed {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !placed {
|
||||
clusters = append(clusters, []*pendingArticleCandidate{value})
|
||||
}
|
||||
}
|
||||
return clusters
|
||||
}
|
||||
|
||||
func articleCandidatesBelongTogether(a, b *pendingArticleCandidate) bool {
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
ids := map[string]bool{}
|
||||
for _, seed := range a.Seeds {
|
||||
ids[seed.ID] = true
|
||||
}
|
||||
for _, seed := range b.Seeds {
|
||||
if ids[seed.ID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
textA := articleCandidateTerms(a)
|
||||
textB := articleCandidateTerms(b)
|
||||
termScore := boolSetJaccard(textA, textB)
|
||||
if termScore >= .42 {
|
||||
return true
|
||||
}
|
||||
catA := map[string]bool{}
|
||||
catB := map[string]bool{}
|
||||
for _, seed := range a.Seeds {
|
||||
for _, cat := range seed.Categories {
|
||||
catA[strings.ToLower(strings.TrimSpace(cat))] = true
|
||||
}
|
||||
}
|
||||
for _, seed := range b.Seeds {
|
||||
for _, cat := range seed.Categories {
|
||||
catB[strings.ToLower(strings.TrimSpace(cat))] = true
|
||||
}
|
||||
}
|
||||
return termScore >= .18 && boolSetJaccard(catA, catB) >= .50
|
||||
}
|
||||
|
||||
func articleCandidateTerms(value *pendingArticleCandidate) map[string]bool {
|
||||
parts := []string{value.Relation.TopicLabel}
|
||||
parts = append(parts, value.Relation.Keywords...)
|
||||
for _, seed := range value.Seeds {
|
||||
parts = append(parts, seed.Label)
|
||||
}
|
||||
return researchTerms(strings.Join(parts, " "))
|
||||
}
|
||||
|
||||
func boolSetJaccard(a, b map[string]bool) float64 {
|
||||
if len(a) == 0 || len(b) == 0 {
|
||||
return 0
|
||||
}
|
||||
intersection := 0
|
||||
union := map[string]bool{}
|
||||
for key := range a {
|
||||
union[key] = true
|
||||
if b[key] {
|
||||
intersection++
|
||||
}
|
||||
}
|
||||
for key := range b {
|
||||
union[key] = true
|
||||
}
|
||||
if len(union) == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(intersection) / float64(len(union))
|
||||
}
|
||||
|
||||
func combinePendingArticleCluster(cluster []*pendingArticleCandidate) ([]model.Node, model.RelationDecision, []model.ResearchResult) {
|
||||
seedByID := map[string]model.Node{}
|
||||
keywords := []string{}
|
||||
topics := []string{}
|
||||
relationTypes := map[string]int{}
|
||||
confidenceSum := 0.0
|
||||
count := 0
|
||||
researchResults := []model.ResearchResult{}
|
||||
for _, item := range cluster {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
for _, seed := range item.Seeds {
|
||||
seedByID[seed.ID] = seed
|
||||
}
|
||||
keywords = append(keywords, item.Relation.Keywords...)
|
||||
if topic := strings.TrimSpace(item.Relation.TopicLabel); topic != "" {
|
||||
topics = append(topics, topic)
|
||||
}
|
||||
if rel := safeRelation(item.Relation.RelationType); rel != "" {
|
||||
relationTypes[rel]++
|
||||
}
|
||||
confidenceSum += item.Relation.Confidence
|
||||
count++
|
||||
researchResults = append(researchResults, item.Research...)
|
||||
}
|
||||
seeds := make([]model.Node, 0, len(seedByID))
|
||||
for _, seed := range seedByID {
|
||||
seeds = append(seeds, seed)
|
||||
}
|
||||
topic := ""
|
||||
if len(topics) > 0 {
|
||||
topic = topics[0]
|
||||
}
|
||||
relationType := "same_topic"
|
||||
maxCount := 0
|
||||
for rel, n := range relationTypes {
|
||||
if n > maxCount {
|
||||
relationType, maxCount = rel, n
|
||||
}
|
||||
}
|
||||
confidence := .8
|
||||
if count > 0 {
|
||||
confidence = confidenceSum / float64(count)
|
||||
}
|
||||
decision := model.RelationDecision{
|
||||
Related: true,
|
||||
RelationType: relationType,
|
||||
Confidence: confidence,
|
||||
TopicLabel: topic,
|
||||
Keywords: unique(keywords),
|
||||
Explanation: fmt.Sprintf("%d thematisch kompatible neue Relationen wurden für einen gemeinsamen Artikelauftrag gebündelt.", count),
|
||||
}
|
||||
return seeds, decision, uniqueResearchEvidence(researchResults)
|
||||
}
|
||||
|
||||
func (e *Engine) synthesizePendingArticleClusters(ctx context.Context, trigger string, pending []*pendingArticleCandidate) (created, skipped int) {
|
||||
clusters := clusterPendingArticleCandidates(pending)
|
||||
for index, cluster := range clusters {
|
||||
seeds, relation, researchResults := combinePendingArticleCluster(cluster)
|
||||
if len(seeds) == 0 {
|
||||
continue
|
||||
}
|
||||
e.Broker.Publish(model.Activity{Type: "article.cluster.started", Source: "brain", Phase: "knowledge-planning", NodeIDs: nodeIDsFromNodes(seeds), Message: fmt.Sprintf("%d Relation(en) werden als gemeinsamer Artikelauftrag verarbeitet", len(cluster)), Strength: .62, Metadata: map[string]any{"trigger": trigger, "cluster_index": index + 1, "relation_count": len(cluster), "seed_count": len(seeds), "processing_mode": "clustered"}})
|
||||
outcome, err := e.synthesizeKnowledgeArticle(ctx, trigger, seeds, relation, researchResults)
|
||||
if err != nil {
|
||||
skipped++
|
||||
e.Broker.Publish(model.Activity{Type: "article.failed", Source: "brain", Phase: "knowledge-synthesis", NodeIDs: nodeIDsFromNodes(seeds), Message: "Gebündelte Artikelsynthese ist fehlgeschlagen; die bereits erzeugten Relationen bleiben erhalten", Strength: .4, Metadata: map[string]any{"trigger": trigger, "cluster_index": index + 1, "relation_count": len(cluster), "error": err.Error()}})
|
||||
continue
|
||||
}
|
||||
if outcome.Created {
|
||||
created++
|
||||
}
|
||||
if outcome.Skipped {
|
||||
skipped++
|
||||
}
|
||||
}
|
||||
return created, skipped
|
||||
}
|
||||
Reference in New Issue
Block a user