claude-brain 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -0
- package/VERSION +1 -0
- package/assets/CLAUDE.md +307 -0
- package/bunfig.toml +8 -0
- package/package.json +74 -0
- package/src/automation/auto-context.ts +240 -0
- package/src/automation/decision-detector.ts +452 -0
- package/src/automation/index.ts +11 -0
- package/src/automation/proactive-recall.ts +373 -0
- package/src/automation/project-detector.ts +297 -0
- package/src/cli/auto-setup.ts +74 -0
- package/src/cli/bin.ts +110 -0
- package/src/cli/commands/install-mcp.ts +50 -0
- package/src/cli/commands/serve.ts +129 -0
- package/src/cli/diagnose.ts +4 -0
- package/src/cli/health-check.ts +4 -0
- package/src/cli/migrate-chroma.ts +106 -0
- package/src/cli/setup.ts +4 -0
- package/src/config/defaults.ts +47 -0
- package/src/config/home.ts +55 -0
- package/src/config/index.ts +7 -0
- package/src/config/loader.ts +166 -0
- package/src/config/migration.ts +76 -0
- package/src/config/schema.ts +257 -0
- package/src/config/validator.ts +184 -0
- package/src/config/watcher.ts +86 -0
- package/src/context/assembler.ts +398 -0
- package/src/context/cache-manager.ts +101 -0
- package/src/context/formatter.ts +84 -0
- package/src/context/hierarchy.ts +85 -0
- package/src/context/index.ts +83 -0
- package/src/context/progress-tracker.ts +174 -0
- package/src/context/standards-manager.ts +267 -0
- package/src/context/types.ts +252 -0
- package/src/context/validator.ts +58 -0
- package/src/cross-project/affinity.ts +162 -0
- package/src/cross-project/generalizer.ts +283 -0
- package/src/cross-project/index.ts +13 -0
- package/src/cross-project/transfer.ts +201 -0
- package/src/diagnostics/index.ts +123 -0
- package/src/health/index.ts +229 -0
- package/src/index.ts +7 -0
- package/src/knowledge/entity-extractor.ts +416 -0
- package/src/knowledge/graph/builder.ts +159 -0
- package/src/knowledge/graph/linker.ts +201 -0
- package/src/knowledge/graph/memory-graph.ts +359 -0
- package/src/knowledge/graph/schema.ts +99 -0
- package/src/knowledge/graph/search.ts +168 -0
- package/src/knowledge/relationship-extractor.ts +108 -0
- package/src/memory/chroma/client.ts +169 -0
- package/src/memory/chroma/collection-manager.ts +94 -0
- package/src/memory/chroma/config.ts +46 -0
- package/src/memory/chroma/embeddings.ts +153 -0
- package/src/memory/chroma/index.ts +82 -0
- package/src/memory/chroma/migration.ts +270 -0
- package/src/memory/chroma/schemas.ts +69 -0
- package/src/memory/chroma/search.ts +315 -0
- package/src/memory/chroma/store.ts +694 -0
- package/src/memory/consolidation/archiver.ts +164 -0
- package/src/memory/consolidation/merger.ts +186 -0
- package/src/memory/consolidation/scorer.ts +138 -0
- package/src/memory/context-builder.ts +236 -0
- package/src/memory/database.ts +169 -0
- package/src/memory/embedding-utils.ts +156 -0
- package/src/memory/embeddings.ts +226 -0
- package/src/memory/episodic/detector.ts +108 -0
- package/src/memory/episodic/manager.ts +334 -0
- package/src/memory/episodic/summarizer.ts +179 -0
- package/src/memory/episodic/types.ts +52 -0
- package/src/memory/index.ts +395 -0
- package/src/memory/knowledge-extractor.ts +455 -0
- package/src/memory/learning.ts +378 -0
- package/src/memory/patterns.ts +396 -0
- package/src/memory/schema.ts +56 -0
- package/src/memory/search.ts +309 -0
- package/src/memory/store.ts +344 -0
- package/src/memory/types.ts +121 -0
- package/src/optimization/index.ts +10 -0
- package/src/optimization/precompute.ts +202 -0
- package/src/optimization/semantic-cache.ts +207 -0
- package/src/orchestrator/coordinator.ts +272 -0
- package/src/orchestrator/decision-logger.ts +228 -0
- package/src/orchestrator/event-emitter.ts +198 -0
- package/src/orchestrator/event-queue.ts +184 -0
- package/src/orchestrator/handlers/base-handler.ts +70 -0
- package/src/orchestrator/handlers/context-handler.ts +73 -0
- package/src/orchestrator/handlers/decision-handler.ts +204 -0
- package/src/orchestrator/handlers/index.ts +10 -0
- package/src/orchestrator/handlers/status-handler.ts +131 -0
- package/src/orchestrator/handlers/task-handler.ts +171 -0
- package/src/orchestrator/index.ts +275 -0
- package/src/orchestrator/task-parser.ts +284 -0
- package/src/orchestrator/types.ts +98 -0
- package/src/phase12/index.ts +456 -0
- package/src/prediction/context-anticipator.ts +198 -0
- package/src/prediction/decision-predictor.ts +184 -0
- package/src/prediction/index.ts +13 -0
- package/src/prediction/recommender.ts +268 -0
- package/src/reasoning/chain-retrieval.ts +247 -0
- package/src/reasoning/counterfactual.ts +248 -0
- package/src/reasoning/index.ts +13 -0
- package/src/reasoning/synthesizer.ts +169 -0
- package/src/retrieval/bm25/index.ts +300 -0
- package/src/retrieval/bm25/tokenizer.ts +184 -0
- package/src/retrieval/feedback/adaptive.ts +223 -0
- package/src/retrieval/feedback/index.ts +16 -0
- package/src/retrieval/feedback/metrics.ts +223 -0
- package/src/retrieval/feedback/store.ts +283 -0
- package/src/retrieval/fusion/index.ts +194 -0
- package/src/retrieval/fusion/rrf.ts +163 -0
- package/src/retrieval/index.ts +12 -0
- package/src/retrieval/pipeline.ts +375 -0
- package/src/retrieval/query/expander.ts +198 -0
- package/src/retrieval/query/index.ts +27 -0
- package/src/retrieval/query/intent-classifier.ts +236 -0
- package/src/retrieval/query/temporal-parser.ts +295 -0
- package/src/retrieval/reranker/index.ts +188 -0
- package/src/retrieval/reranker/model.ts +95 -0
- package/src/retrieval/service.ts +125 -0
- package/src/retrieval/types.ts +162 -0
- package/src/scripts/health-check.ts +118 -0
- package/src/scripts/setup.ts +122 -0
- package/src/server/handlers/call-tool.ts +194 -0
- package/src/server/handlers/index.ts +9 -0
- package/src/server/handlers/list-tools.ts +18 -0
- package/src/server/handlers/tools/analyze-decision-evolution.ts +71 -0
- package/src/server/handlers/tools/auto-remember.ts +200 -0
- package/src/server/handlers/tools/create-project.ts +135 -0
- package/src/server/handlers/tools/detect-trends.ts +80 -0
- package/src/server/handlers/tools/find-cross-project-patterns.ts +73 -0
- package/src/server/handlers/tools/get-activity-log.ts +194 -0
- package/src/server/handlers/tools/get-code-standards.ts +124 -0
- package/src/server/handlers/tools/get-corrections.ts +154 -0
- package/src/server/handlers/tools/get-decision-timeline.ts +86 -0
- package/src/server/handlers/tools/get-episode.ts +93 -0
- package/src/server/handlers/tools/get-patterns.ts +158 -0
- package/src/server/handlers/tools/get-phase12-status.ts +63 -0
- package/src/server/handlers/tools/get-project-context.ts +75 -0
- package/src/server/handlers/tools/get-recommendations.ts +65 -0
- package/src/server/handlers/tools/index.ts +33 -0
- package/src/server/handlers/tools/init-project.ts +710 -0
- package/src/server/handlers/tools/list-episodes.ts +80 -0
- package/src/server/handlers/tools/list-projects.ts +125 -0
- package/src/server/handlers/tools/rate-memory.ts +95 -0
- package/src/server/handlers/tools/recall-similar.ts +87 -0
- package/src/server/handlers/tools/recognize-pattern.ts +126 -0
- package/src/server/handlers/tools/record-correction.ts +125 -0
- package/src/server/handlers/tools/remember-decision.ts +153 -0
- package/src/server/handlers/tools/schemas.ts +241 -0
- package/src/server/handlers/tools/search-knowledge-graph.ts +89 -0
- package/src/server/handlers/tools/smart-context.ts +124 -0
- package/src/server/handlers/tools/update-progress.ts +114 -0
- package/src/server/handlers/tools/what-if-analysis.ts +73 -0
- package/src/server/http-api.ts +474 -0
- package/src/server/index.ts +40 -0
- package/src/server/mcp-server.ts +283 -0
- package/src/server/providers/index.ts +7 -0
- package/src/server/providers/prompts.ts +327 -0
- package/src/server/providers/resources.ts +427 -0
- package/src/server/services.ts +388 -0
- package/src/server/types.ts +39 -0
- package/src/server/utils/error-handler.ts +155 -0
- package/src/server/utils/index.ts +13 -0
- package/src/server/utils/memory-indicator.ts +83 -0
- package/src/server/utils/request-context.ts +122 -0
- package/src/server/utils/response-formatter.ts +124 -0
- package/src/server/utils/validators.ts +210 -0
- package/src/setup/index.ts +22 -0
- package/src/setup/wizard.ts +321 -0
- package/src/temporal/evolution.ts +197 -0
- package/src/temporal/index.ts +16 -0
- package/src/temporal/query-processor.ts +190 -0
- package/src/temporal/timeline.ts +259 -0
- package/src/temporal/trends.ts +263 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/registry.ts +106 -0
- package/src/tools/schemas.test.ts +30 -0
- package/src/tools/schemas.ts +907 -0
- package/src/tools/types.ts +412 -0
- package/src/utils/circuit-breaker.ts +130 -0
- package/src/utils/cleanup.ts +34 -0
- package/src/utils/error-handler.ts +132 -0
- package/src/utils/error-messages.ts +60 -0
- package/src/utils/fallback.ts +45 -0
- package/src/utils/index.ts +54 -0
- package/src/utils/logger-utils.ts +80 -0
- package/src/utils/logger.ts +88 -0
- package/src/utils/phase12-helper.ts +56 -0
- package/src/utils/retry.ts +94 -0
- package/src/utils/transaction.ts +63 -0
- package/src/vault/frontmatter.ts +264 -0
- package/src/vault/index.ts +318 -0
- package/src/vault/paths.ts +106 -0
- package/src/vault/query.ts +422 -0
- package/src/vault/reader.ts +264 -0
- package/src/vault/templates.ts +186 -0
- package/src/vault/types.ts +73 -0
- package/src/vault/watcher.ts +277 -0
- package/src/vault/writer.ts +393 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Module Exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { FeedbackStore } from './store'
|
|
6
|
+
export {
|
|
7
|
+
FeedbackMetrics,
|
|
8
|
+
calculateMRR,
|
|
9
|
+
calculatePrecisionAtK,
|
|
10
|
+
calculateNDCG,
|
|
11
|
+
calculateAllMetrics,
|
|
12
|
+
calculateAverageRating,
|
|
13
|
+
calculatePositiveRate,
|
|
14
|
+
calculateUsageRate
|
|
15
|
+
} from './metrics'
|
|
16
|
+
export { AdaptiveLearner, type AdaptiveLearnerConfig } from './adaptive'
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieval Metrics
|
|
3
|
+
* Evaluation metrics for retrieval quality: MRR, Precision@K, NDCG
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { MemoryFeedback, RetrievalMetrics } from '../types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Calculate Mean Reciprocal Rank (MRR)
|
|
10
|
+
*
|
|
11
|
+
* MRR measures where the first relevant result appears in the ranking.
|
|
12
|
+
* MRR = (1/|Q|) * sum(1/rank_i) for each query
|
|
13
|
+
*
|
|
14
|
+
* @param feedbackByQuery - Feedback grouped by query
|
|
15
|
+
* @param relevanceThreshold - Minimum rating to consider relevant (default: 4)
|
|
16
|
+
*/
|
|
17
|
+
export function calculateMRR(
|
|
18
|
+
feedbackByQuery: Map<string, MemoryFeedback[]>,
|
|
19
|
+
relevanceThreshold: number = 4
|
|
20
|
+
): number {
|
|
21
|
+
if (feedbackByQuery.size === 0) return 0
|
|
22
|
+
|
|
23
|
+
let sumReciprocal = 0
|
|
24
|
+
let queryCount = 0
|
|
25
|
+
|
|
26
|
+
for (const [, feedbacks] of feedbackByQuery) {
|
|
27
|
+
// Sort by implicit rank (assuming feedback order matches retrieval order)
|
|
28
|
+
const sorted = [...feedbacks]
|
|
29
|
+
|
|
30
|
+
// Find first relevant result
|
|
31
|
+
let foundRelevant = false
|
|
32
|
+
for (let rank = 0; rank < sorted.length; rank++) {
|
|
33
|
+
if (sorted[rank].rating >= relevanceThreshold) {
|
|
34
|
+
sumReciprocal += 1 / (rank + 1)
|
|
35
|
+
foundRelevant = true
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sorted.length > 0) {
|
|
41
|
+
queryCount++
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return queryCount > 0 ? sumReciprocal / queryCount : 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate Precision at K
|
|
50
|
+
*
|
|
51
|
+
* Precision@K = (# relevant in top K) / K
|
|
52
|
+
*
|
|
53
|
+
* @param feedbackByQuery - Feedback grouped by query
|
|
54
|
+
* @param k - Number of top results to consider
|
|
55
|
+
* @param relevanceThreshold - Minimum rating to consider relevant
|
|
56
|
+
*/
|
|
57
|
+
export function calculatePrecisionAtK(
|
|
58
|
+
feedbackByQuery: Map<string, MemoryFeedback[]>,
|
|
59
|
+
k: number,
|
|
60
|
+
relevanceThreshold: number = 4
|
|
61
|
+
): number {
|
|
62
|
+
if (feedbackByQuery.size === 0) return 0
|
|
63
|
+
|
|
64
|
+
let sumPrecision = 0
|
|
65
|
+
let queryCount = 0
|
|
66
|
+
|
|
67
|
+
for (const [, feedbacks] of feedbackByQuery) {
|
|
68
|
+
const topK = feedbacks.slice(0, k)
|
|
69
|
+
if (topK.length === 0) continue
|
|
70
|
+
|
|
71
|
+
const relevantCount = topK.filter(f => f.rating >= relevanceThreshold).length
|
|
72
|
+
sumPrecision += relevantCount / k
|
|
73
|
+
|
|
74
|
+
queryCount++
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return queryCount > 0 ? sumPrecision / queryCount : 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Calculate Normalized Discounted Cumulative Gain (NDCG)
|
|
82
|
+
*
|
|
83
|
+
* NDCG measures ranking quality, giving higher weight to relevant results at the top.
|
|
84
|
+
* DCG = sum(rating_i / log2(i + 1))
|
|
85
|
+
* NDCG = DCG / IDCG (ideal DCG)
|
|
86
|
+
*
|
|
87
|
+
* @param feedbackByQuery - Feedback grouped by query
|
|
88
|
+
* @param k - Number of top results to consider (optional, default all)
|
|
89
|
+
*/
|
|
90
|
+
export function calculateNDCG(
|
|
91
|
+
feedbackByQuery: Map<string, MemoryFeedback[]>,
|
|
92
|
+
k?: number
|
|
93
|
+
): number {
|
|
94
|
+
if (feedbackByQuery.size === 0) return 0
|
|
95
|
+
|
|
96
|
+
let sumNDCG = 0
|
|
97
|
+
let queryCount = 0
|
|
98
|
+
|
|
99
|
+
for (const [, feedbacks] of feedbackByQuery) {
|
|
100
|
+
if (feedbacks.length === 0) continue
|
|
101
|
+
|
|
102
|
+
const results = k ? feedbacks.slice(0, k) : feedbacks
|
|
103
|
+
|
|
104
|
+
// Calculate DCG
|
|
105
|
+
let dcg = 0
|
|
106
|
+
for (let i = 0; i < results.length; i++) {
|
|
107
|
+
// Use rating as relevance score (1-5)
|
|
108
|
+
const relevance = results[i].rating
|
|
109
|
+
dcg += relevance / Math.log2(i + 2) // +2 because log2(1) = 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Calculate ideal DCG (sort by rating descending)
|
|
113
|
+
const idealResults = [...results].sort((a, b) => b.rating - a.rating)
|
|
114
|
+
let idcg = 0
|
|
115
|
+
for (let i = 0; i < idealResults.length; i++) {
|
|
116
|
+
const relevance = idealResults[i].rating
|
|
117
|
+
idcg += relevance / Math.log2(i + 2)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Normalize
|
|
121
|
+
if (idcg > 0) {
|
|
122
|
+
sumNDCG += dcg / idcg
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
queryCount++
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return queryCount > 0 ? sumNDCG / queryCount : 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Calculate all retrieval metrics from feedback
|
|
133
|
+
*/
|
|
134
|
+
export function calculateAllMetrics(
|
|
135
|
+
feedback: MemoryFeedback[],
|
|
136
|
+
kValues: number[] = [3, 5, 10]
|
|
137
|
+
): RetrievalMetrics {
|
|
138
|
+
// Group feedback by query
|
|
139
|
+
const feedbackByQuery = new Map<string, MemoryFeedback[]>()
|
|
140
|
+
|
|
141
|
+
for (const f of feedback) {
|
|
142
|
+
const existing = feedbackByQuery.get(f.queryId) || []
|
|
143
|
+
existing.push(f)
|
|
144
|
+
feedbackByQuery.set(f.queryId, existing)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Calculate precision at each K
|
|
148
|
+
const precisionAtK: Record<number, number> = {}
|
|
149
|
+
for (const k of kValues) {
|
|
150
|
+
precisionAtK[k] = calculatePrecisionAtK(feedbackByQuery, k)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
mrr: calculateMRR(feedbackByQuery),
|
|
155
|
+
precisionAtK,
|
|
156
|
+
ndcg: calculateNDCG(feedbackByQuery),
|
|
157
|
+
queryCount: feedbackByQuery.size,
|
|
158
|
+
avgLatencyMs: 0 // Not tracked in feedback
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Calculate average rating from feedback
|
|
164
|
+
*/
|
|
165
|
+
export function calculateAverageRating(feedback: MemoryFeedback[]): number {
|
|
166
|
+
if (feedback.length === 0) return 0
|
|
167
|
+
const sum = feedback.reduce((acc, f) => acc + f.rating, 0)
|
|
168
|
+
return sum / feedback.length
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Calculate positive feedback rate (rating >= 4)
|
|
173
|
+
*/
|
|
174
|
+
export function calculatePositiveRate(
|
|
175
|
+
feedback: MemoryFeedback[],
|
|
176
|
+
threshold: number = 4
|
|
177
|
+
): number {
|
|
178
|
+
if (feedback.length === 0) return 0
|
|
179
|
+
const positive = feedback.filter(f => f.rating >= threshold).length
|
|
180
|
+
return positive / feedback.length
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Calculate usage rate (was_used = true)
|
|
185
|
+
*/
|
|
186
|
+
export function calculateUsageRate(feedback: MemoryFeedback[]): number {
|
|
187
|
+
if (feedback.length === 0) return 0
|
|
188
|
+
const used = feedback.filter(f => f.wasUsed).length
|
|
189
|
+
return used / feedback.length
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export class FeedbackMetrics {
|
|
193
|
+
calculateMRR(feedback: Map<string, MemoryFeedback[]>): number {
|
|
194
|
+
return calculateMRR(feedback)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
calculatePrecisionAtK(
|
|
198
|
+
feedback: Map<string, MemoryFeedback[]>,
|
|
199
|
+
k: number
|
|
200
|
+
): number {
|
|
201
|
+
return calculatePrecisionAtK(feedback, k)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
calculateNDCG(feedback: Map<string, MemoryFeedback[]>): number {
|
|
205
|
+
return calculateNDCG(feedback)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
calculateAll(feedback: MemoryFeedback[]): RetrievalMetrics {
|
|
209
|
+
return calculateAllMetrics(feedback)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
calculateAverageRating(feedback: MemoryFeedback[]): number {
|
|
213
|
+
return calculateAverageRating(feedback)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
calculatePositiveRate(feedback: MemoryFeedback[]): number {
|
|
217
|
+
return calculatePositiveRate(feedback)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
calculateUsageRate(feedback: MemoryFeedback[]): number {
|
|
221
|
+
return calculateUsageRate(feedback)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Store
|
|
3
|
+
* Stores and retrieves memory feedback for adaptive learning
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from 'pino'
|
|
7
|
+
import type { CollectionManager } from '@/memory/chroma/collection-manager'
|
|
8
|
+
import type { EmbeddingProvider } from '@/memory/chroma/embeddings'
|
|
9
|
+
import type { MemoryFeedback, RateMemoryInput } from '../types'
|
|
10
|
+
|
|
11
|
+
export class FeedbackStore {
|
|
12
|
+
private logger: Logger
|
|
13
|
+
private collections: CollectionManager
|
|
14
|
+
private embeddings: EmbeddingProvider
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
logger: Logger,
|
|
18
|
+
collections: CollectionManager,
|
|
19
|
+
embeddings: EmbeddingProvider
|
|
20
|
+
) {
|
|
21
|
+
this.logger = logger.child({ component: 'feedback-store' })
|
|
22
|
+
this.collections = collections
|
|
23
|
+
this.embeddings = embeddings
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Store feedback for a memory retrieval
|
|
28
|
+
*/
|
|
29
|
+
async storeFeedback(input: RateMemoryInput): Promise<string> {
|
|
30
|
+
const feedbackId = `feedback_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
31
|
+
const queryId = `query_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
32
|
+
const timestamp = new Date().toISOString()
|
|
33
|
+
|
|
34
|
+
this.logger.debug({
|
|
35
|
+
feedbackId,
|
|
36
|
+
memoryId: input.memory_id,
|
|
37
|
+
rating: input.rating
|
|
38
|
+
}, 'Storing feedback')
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const collection = await this.collections.getFeedback()
|
|
42
|
+
|
|
43
|
+
// Generate embedding for the query to enable similarity search
|
|
44
|
+
const embedding = await this.embeddings.generate(input.query)
|
|
45
|
+
|
|
46
|
+
// Determine collection type from memory_id prefix
|
|
47
|
+
const collectionType = this.inferCollectionType(input.memory_id)
|
|
48
|
+
|
|
49
|
+
await collection.add({
|
|
50
|
+
ids: [feedbackId],
|
|
51
|
+
embeddings: [embedding],
|
|
52
|
+
documents: [input.query],
|
|
53
|
+
metadatas: [{
|
|
54
|
+
memory_id: input.memory_id,
|
|
55
|
+
query_id: queryId,
|
|
56
|
+
query: input.query,
|
|
57
|
+
rating: input.rating,
|
|
58
|
+
was_used: input.was_used ?? true,
|
|
59
|
+
feedback_text: input.feedback_text || '',
|
|
60
|
+
project: input.project_name || '',
|
|
61
|
+
created_at: timestamp,
|
|
62
|
+
collection_type: collectionType
|
|
63
|
+
}]
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
this.logger.info({
|
|
67
|
+
feedbackId,
|
|
68
|
+
memoryId: input.memory_id,
|
|
69
|
+
rating: input.rating
|
|
70
|
+
}, 'Feedback stored successfully')
|
|
71
|
+
|
|
72
|
+
return feedbackId
|
|
73
|
+
|
|
74
|
+
} catch (error) {
|
|
75
|
+
this.logger.error({ error, input }, 'Failed to store feedback')
|
|
76
|
+
throw error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get feedback for a specific memory
|
|
82
|
+
*/
|
|
83
|
+
async getFeedbackForMemory(memoryId: string): Promise<MemoryFeedback[]> {
|
|
84
|
+
try {
|
|
85
|
+
const collection = await this.collections.getFeedback()
|
|
86
|
+
|
|
87
|
+
const results = await collection.get({
|
|
88
|
+
where: { memory_id: { $eq: memoryId } },
|
|
89
|
+
include: ['documents', 'metadatas']
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (!results.ids.length) {
|
|
93
|
+
return []
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results.ids.map((id, index) => ({
|
|
97
|
+
id,
|
|
98
|
+
memoryId: results.metadatas?.[index]?.memory_id as string || '',
|
|
99
|
+
queryId: results.metadatas?.[index]?.query_id as string || '',
|
|
100
|
+
query: results.documents?.[index] || '',
|
|
101
|
+
rating: results.metadatas?.[index]?.rating as 1 | 2 | 3 | 4 | 5,
|
|
102
|
+
wasUsed: results.metadatas?.[index]?.was_used as boolean ?? true,
|
|
103
|
+
feedbackText: results.metadatas?.[index]?.feedback_text as string | undefined,
|
|
104
|
+
project: results.metadatas?.[index]?.project as string | undefined,
|
|
105
|
+
timestamp: results.metadatas?.[index]?.created_at as string || '',
|
|
106
|
+
collectionType: results.metadatas?.[index]?.collection_type as 'decisions' | 'memories' | 'patterns' | 'corrections'
|
|
107
|
+
}))
|
|
108
|
+
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.logger.error({ error, memoryId }, 'Failed to get feedback for memory')
|
|
111
|
+
return []
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get all feedback for a project
|
|
117
|
+
*/
|
|
118
|
+
async getFeedbackForProject(project: string, limit: number = 100): Promise<MemoryFeedback[]> {
|
|
119
|
+
try {
|
|
120
|
+
const collection = await this.collections.getFeedback()
|
|
121
|
+
|
|
122
|
+
const results = await collection.get({
|
|
123
|
+
where: { project: { $eq: project } },
|
|
124
|
+
include: ['documents', 'metadatas'],
|
|
125
|
+
limit
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (!results.ids.length) {
|
|
129
|
+
return []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return results.ids.map((id, index) => ({
|
|
133
|
+
id,
|
|
134
|
+
memoryId: results.metadatas?.[index]?.memory_id as string || '',
|
|
135
|
+
queryId: results.metadatas?.[index]?.query_id as string || '',
|
|
136
|
+
query: results.documents?.[index] || '',
|
|
137
|
+
rating: results.metadatas?.[index]?.rating as 1 | 2 | 3 | 4 | 5,
|
|
138
|
+
wasUsed: results.metadatas?.[index]?.was_used as boolean ?? true,
|
|
139
|
+
feedbackText: results.metadatas?.[index]?.feedback_text as string | undefined,
|
|
140
|
+
project: results.metadatas?.[index]?.project as string | undefined,
|
|
141
|
+
timestamp: results.metadatas?.[index]?.created_at as string || '',
|
|
142
|
+
collectionType: results.metadatas?.[index]?.collection_type as 'decisions' | 'memories' | 'patterns' | 'corrections'
|
|
143
|
+
}))
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.logger.error({ error, project }, 'Failed to get feedback for project')
|
|
147
|
+
return []
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get recent feedback (for adaptive learning)
|
|
153
|
+
*/
|
|
154
|
+
async getRecentFeedback(limit: number = 100): Promise<MemoryFeedback[]> {
|
|
155
|
+
try {
|
|
156
|
+
const collection = await this.collections.getFeedback()
|
|
157
|
+
|
|
158
|
+
// Get all feedback - ChromaDB doesn't support ordering, so we'll sort in memory
|
|
159
|
+
const results = await collection.get({
|
|
160
|
+
include: ['documents', 'metadatas'],
|
|
161
|
+
limit: limit * 2 // Get more to allow for sorting
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if (!results.ids.length) {
|
|
165
|
+
return []
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const feedback: MemoryFeedback[] = results.ids.map((id, index) => ({
|
|
169
|
+
id,
|
|
170
|
+
memoryId: results.metadatas?.[index]?.memory_id as string || '',
|
|
171
|
+
queryId: results.metadatas?.[index]?.query_id as string || '',
|
|
172
|
+
query: results.documents?.[index] || '',
|
|
173
|
+
rating: results.metadatas?.[index]?.rating as 1 | 2 | 3 | 4 | 5,
|
|
174
|
+
wasUsed: results.metadatas?.[index]?.was_used as boolean ?? true,
|
|
175
|
+
feedbackText: results.metadatas?.[index]?.feedback_text as string | undefined,
|
|
176
|
+
project: results.metadatas?.[index]?.project as string | undefined,
|
|
177
|
+
timestamp: results.metadatas?.[index]?.created_at as string || '',
|
|
178
|
+
collectionType: results.metadatas?.[index]?.collection_type as 'decisions' | 'memories' | 'patterns' | 'corrections'
|
|
179
|
+
}))
|
|
180
|
+
|
|
181
|
+
// Sort by timestamp descending and take limit
|
|
182
|
+
return feedback
|
|
183
|
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
184
|
+
.slice(0, limit)
|
|
185
|
+
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.logger.error({ error }, 'Failed to get recent feedback')
|
|
188
|
+
return []
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get feedback statistics
|
|
194
|
+
*/
|
|
195
|
+
async getStats(): Promise<{
|
|
196
|
+
totalFeedback: number
|
|
197
|
+
avgRating: number
|
|
198
|
+
positiveRate: number
|
|
199
|
+
usageRate: number
|
|
200
|
+
byCollection: Record<string, { count: number; avgRating: number }>
|
|
201
|
+
}> {
|
|
202
|
+
try {
|
|
203
|
+
const collection = await this.collections.getFeedback()
|
|
204
|
+
const totalCount = await collection.count()
|
|
205
|
+
|
|
206
|
+
if (totalCount === 0) {
|
|
207
|
+
return {
|
|
208
|
+
totalFeedback: 0,
|
|
209
|
+
avgRating: 0,
|
|
210
|
+
positiveRate: 0,
|
|
211
|
+
usageRate: 0,
|
|
212
|
+
byCollection: {}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const results = await collection.get({
|
|
217
|
+
include: ['metadatas'],
|
|
218
|
+
limit: 1000 // Sample for stats
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
let totalRating = 0
|
|
222
|
+
let positiveCount = 0
|
|
223
|
+
let usedCount = 0
|
|
224
|
+
const byCollection: Record<string, { count: number; totalRating: number }> = {}
|
|
225
|
+
|
|
226
|
+
for (const metadata of results.metadatas || []) {
|
|
227
|
+
const rating = metadata?.rating as number
|
|
228
|
+
const wasUsed = metadata?.was_used as boolean
|
|
229
|
+
const collectionType = (metadata?.collection_type as string) || 'unknown'
|
|
230
|
+
|
|
231
|
+
totalRating += rating
|
|
232
|
+
if (rating >= 4) positiveCount++
|
|
233
|
+
if (wasUsed) usedCount++
|
|
234
|
+
|
|
235
|
+
if (!byCollection[collectionType]) {
|
|
236
|
+
byCollection[collectionType] = { count: 0, totalRating: 0 }
|
|
237
|
+
}
|
|
238
|
+
byCollection[collectionType].count++
|
|
239
|
+
byCollection[collectionType].totalRating += rating
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const count = results.ids.length
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
totalFeedback: totalCount,
|
|
246
|
+
avgRating: count > 0 ? totalRating / count : 0,
|
|
247
|
+
positiveRate: count > 0 ? positiveCount / count : 0,
|
|
248
|
+
usageRate: count > 0 ? usedCount / count : 0,
|
|
249
|
+
byCollection: Object.fromEntries(
|
|
250
|
+
Object.entries(byCollection).map(([key, value]) => [
|
|
251
|
+
key,
|
|
252
|
+
{
|
|
253
|
+
count: value.count,
|
|
254
|
+
avgRating: value.count > 0 ? value.totalRating / value.count : 0
|
|
255
|
+
}
|
|
256
|
+
])
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
} catch (error) {
|
|
261
|
+
this.logger.error({ error }, 'Failed to get feedback stats')
|
|
262
|
+
return {
|
|
263
|
+
totalFeedback: 0,
|
|
264
|
+
avgRating: 0,
|
|
265
|
+
positiveRate: 0,
|
|
266
|
+
usageRate: 0,
|
|
267
|
+
byCollection: {}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Infer collection type from memory ID prefix
|
|
274
|
+
*/
|
|
275
|
+
private inferCollectionType(memoryId: string): 'decisions' | 'memories' | 'patterns' | 'corrections' {
|
|
276
|
+
if (memoryId.startsWith('dec_')) return 'decisions'
|
|
277
|
+
if (memoryId.startsWith('mem_')) return 'memories'
|
|
278
|
+
if (memoryId.startsWith('pat_')) return 'patterns'
|
|
279
|
+
if (memoryId.startsWith('cor_')) return 'corrections'
|
|
280
|
+
// Default to decisions if unknown
|
|
281
|
+
return 'decisions'
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fusion Strategies
|
|
3
|
+
* Different methods for combining dense and sparse search results
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { HybridSearchResult } from '../types'
|
|
7
|
+
import { fuseWithRRF, fuseMultipleWithRRF, calculateRRFScore } from './rrf'
|
|
8
|
+
|
|
9
|
+
export interface FusionConfig {
|
|
10
|
+
/** Fusion method to use */
|
|
11
|
+
method: 'rrf' | 'linear' | 'max'
|
|
12
|
+
/** RRF k parameter (for RRF method) */
|
|
13
|
+
rrfK?: number
|
|
14
|
+
/** Dense weight (for linear method) */
|
|
15
|
+
denseWeight?: number
|
|
16
|
+
/** Sparse weight (for linear method) */
|
|
17
|
+
sparseWeight?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_CONFIG: FusionConfig = {
|
|
21
|
+
method: 'rrf',
|
|
22
|
+
rrfK: 60,
|
|
23
|
+
denseWeight: 0.7,
|
|
24
|
+
sparseWeight: 0.3
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* RRF Fusion - Reciprocal Rank Fusion
|
|
29
|
+
*/
|
|
30
|
+
export class RRFFusion {
|
|
31
|
+
private k: number
|
|
32
|
+
|
|
33
|
+
constructor(k: number = 60) {
|
|
34
|
+
this.k = k
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fuse(
|
|
38
|
+
denseResults: HybridSearchResult[],
|
|
39
|
+
sparseResults: HybridSearchResult[]
|
|
40
|
+
): HybridSearchResult[] {
|
|
41
|
+
return fuseWithRRF(denseResults, sparseResults, { k: this.k })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fuseMultiple(rankedLists: HybridSearchResult[][]): HybridSearchResult[] {
|
|
45
|
+
return fuseMultipleWithRRF(rankedLists, { k: this.k })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Linear Fusion - Weighted combination of scores
|
|
51
|
+
*/
|
|
52
|
+
export class LinearFusion {
|
|
53
|
+
private denseWeight: number
|
|
54
|
+
private sparseWeight: number
|
|
55
|
+
|
|
56
|
+
constructor(denseWeight: number = 0.7, sparseWeight: number = 0.3) {
|
|
57
|
+
this.denseWeight = denseWeight
|
|
58
|
+
this.sparseWeight = sparseWeight
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fuse(
|
|
62
|
+
denseResults: HybridSearchResult[],
|
|
63
|
+
sparseResults: HybridSearchResult[]
|
|
64
|
+
): HybridSearchResult[] {
|
|
65
|
+
// Create map for merging
|
|
66
|
+
const merged = new Map<string, HybridSearchResult>()
|
|
67
|
+
|
|
68
|
+
// Add dense results
|
|
69
|
+
for (const result of denseResults) {
|
|
70
|
+
merged.set(result.id, {
|
|
71
|
+
...result,
|
|
72
|
+
scores: {
|
|
73
|
+
...result.scores,
|
|
74
|
+
fusion: result.scores.dense * this.denseWeight,
|
|
75
|
+
final: result.scores.dense * this.denseWeight
|
|
76
|
+
},
|
|
77
|
+
provenance: 'dense'
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add/merge sparse results
|
|
82
|
+
for (const result of sparseResults) {
|
|
83
|
+
const existing = merged.get(result.id)
|
|
84
|
+
if (existing) {
|
|
85
|
+
const combinedScore =
|
|
86
|
+
existing.scores.fusion + result.scores.sparse * this.sparseWeight
|
|
87
|
+
merged.set(result.id, {
|
|
88
|
+
...existing,
|
|
89
|
+
scores: {
|
|
90
|
+
...existing.scores,
|
|
91
|
+
sparse: result.scores.sparse,
|
|
92
|
+
fusion: combinedScore,
|
|
93
|
+
final: combinedScore
|
|
94
|
+
},
|
|
95
|
+
provenance: 'both'
|
|
96
|
+
})
|
|
97
|
+
} else {
|
|
98
|
+
merged.set(result.id, {
|
|
99
|
+
...result,
|
|
100
|
+
scores: {
|
|
101
|
+
...result.scores,
|
|
102
|
+
fusion: result.scores.sparse * this.sparseWeight,
|
|
103
|
+
final: result.scores.sparse * this.sparseWeight
|
|
104
|
+
},
|
|
105
|
+
provenance: 'sparse'
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Sort by fusion score
|
|
111
|
+
const results = Array.from(merged.values())
|
|
112
|
+
results.sort((a, b) => b.scores.fusion - a.scores.fusion)
|
|
113
|
+
|
|
114
|
+
return results
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Max Fusion - Take maximum score from either list
|
|
120
|
+
*/
|
|
121
|
+
export class MaxFusion {
|
|
122
|
+
fuse(
|
|
123
|
+
denseResults: HybridSearchResult[],
|
|
124
|
+
sparseResults: HybridSearchResult[]
|
|
125
|
+
): HybridSearchResult[] {
|
|
126
|
+
// Create map for merging
|
|
127
|
+
const merged = new Map<string, HybridSearchResult>()
|
|
128
|
+
|
|
129
|
+
// Add dense results
|
|
130
|
+
for (const result of denseResults) {
|
|
131
|
+
merged.set(result.id, {
|
|
132
|
+
...result,
|
|
133
|
+
scores: {
|
|
134
|
+
...result.scores,
|
|
135
|
+
fusion: result.scores.dense,
|
|
136
|
+
final: result.scores.dense
|
|
137
|
+
},
|
|
138
|
+
provenance: 'dense'
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add/merge sparse results
|
|
143
|
+
for (const result of sparseResults) {
|
|
144
|
+
const existing = merged.get(result.id)
|
|
145
|
+
if (existing) {
|
|
146
|
+
const maxScore = Math.max(existing.scores.dense || 0, result.scores.sparse)
|
|
147
|
+
merged.set(result.id, {
|
|
148
|
+
...existing,
|
|
149
|
+
scores: {
|
|
150
|
+
...existing.scores,
|
|
151
|
+
sparse: result.scores.sparse,
|
|
152
|
+
fusion: maxScore,
|
|
153
|
+
final: maxScore
|
|
154
|
+
},
|
|
155
|
+
provenance: 'both'
|
|
156
|
+
})
|
|
157
|
+
} else {
|
|
158
|
+
merged.set(result.id, {
|
|
159
|
+
...result,
|
|
160
|
+
scores: {
|
|
161
|
+
...result.scores,
|
|
162
|
+
fusion: result.scores.sparse,
|
|
163
|
+
final: result.scores.sparse
|
|
164
|
+
},
|
|
165
|
+
provenance: 'sparse'
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Sort by fusion score
|
|
171
|
+
const results = Array.from(merged.values())
|
|
172
|
+
results.sort((a, b) => b.scores.fusion - a.scores.fusion)
|
|
173
|
+
|
|
174
|
+
return results
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Create a fusion strategy based on config
|
|
180
|
+
*/
|
|
181
|
+
export function createFusionStrategy(config: FusionConfig = DEFAULT_CONFIG) {
|
|
182
|
+
switch (config.method) {
|
|
183
|
+
case 'rrf':
|
|
184
|
+
return new RRFFusion(config.rrfK)
|
|
185
|
+
case 'linear':
|
|
186
|
+
return new LinearFusion(config.denseWeight, config.sparseWeight)
|
|
187
|
+
case 'max':
|
|
188
|
+
return new MaxFusion()
|
|
189
|
+
default:
|
|
190
|
+
return new RRFFusion(config.rrfK)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { fuseWithRRF, fuseMultipleWithRRF, calculateRRFScore } from './rrf'
|