forked from Keyoku-ai/keyoku-engine
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpatterns.go
More file actions
121 lines (104 loc) · 3.19 KB
/
Copy pathpatterns.go
File metadata and controls
121 lines (104 loc) · 3.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// SPDX-License-Identifier: BSL-1.1
// Copyright (c) 2026 Keyoku. All rights reserved.
package keyoku
import (
"context"
"fmt"
"strings"
"time"
"github.com/keyoku-ai/keyoku-engine/storage"
)
// BehavioralPattern represents a recurring topic pattern detected on specific days.
type BehavioralPattern struct {
Description string
Confidence float64
DayOfWeek *int // 0=Sunday, 6=Saturday; nil if not day-specific
Topics []string
MemoryIDs []string
}
// detectBehavioralPatterns analyzes memory creation patterns over the last 90 days
// and returns patterns that match today's day of week.
// Uses memory type + tags (LLM-assigned metadata) for topic grouping — no hardcoded keywords.
func (k *Keyoku) detectBehavioralPatterns(ctx context.Context, entityID string) []BehavioralPattern {
memories, err := k.store.QueryMemories(ctx, storage.MemoryQuery{
EntityID: entityID,
States: []storage.MemoryState{storage.StateActive, storage.StateStale},
Limit: 500,
OrderBy: "created_at",
Descending: true,
})
if err != nil || len(memories) == 0 {
return nil
}
// Filter to last 90 days
cutoff := time.Now().AddDate(0, 0, -90)
var recent []*Memory
for _, m := range memories {
if m.CreatedAt.After(cutoff) {
recent = append(recent, m)
}
}
if len(recent) < 10 {
return nil // not enough data
}
// Group by day-of-week using memory metadata (type + tags) as topic signals.
// These are assigned by the LLM extraction pipeline, not hardcoded.
type dayTopic struct {
day int
topic string
}
topicCounts := make(map[dayTopic]int)
topicMemories := make(map[dayTopic][]string)
for _, m := range recent {
dow := int(m.CreatedAt.Weekday())
topics := extractMemoryTopics(m)
for _, topic := range topics {
dt := dayTopic{day: dow, topic: topic}
topicCounts[dt]++
topicMemories[dt] = append(topicMemories[dt], m.ID)
}
}
// Find patterns: same topic appears 3+ times on same day-of-week
today := int(time.Now().Weekday())
var patterns []BehavioralPattern
seen := make(map[string]bool)
for dt, count := range topicCounts {
if count < 3 || dt.day != today {
continue
}
if seen[dt.topic] {
continue
}
seen[dt.topic] = true
dayName := time.Weekday(dt.day).String()
confidence := float64(count) / 10.0
if confidence > 1.0 {
confidence = 1.0
}
dow := dt.day
patterns = append(patterns, BehavioralPattern{
Description: fmt.Sprintf("User typically works on %s on %ss", dt.topic, dayName),
Confidence: confidence,
DayOfWeek: &dow,
Topics: []string{dt.topic},
MemoryIDs: topicMemories[dt],
})
}
return patterns
}
// extractMemoryTopics derives topic labels from a memory's LLM-assigned metadata.
// Uses memory type (assigned by extraction LLM) and tags (also LLM-assigned) —
// no hardcoded keyword matching.
func extractMemoryTopics(m *Memory) []string {
var topics []string
// Memory type is assigned by the LLM extraction pipeline
topics = append(topics, strings.ToLower(string(m.Type)))
// Tags are also LLM-assigned during extraction; skip schedule tags
for _, tag := range m.Tags {
if strings.HasPrefix(tag, "cron:") {
continue
}
topics = append(topics, strings.ToLower(tag))
}
return topics
}