claude-brain 0.17.13 → 0.22.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/VERSION +1 -1
- package/package.json +3 -1
- package/scripts/postinstall.mjs +80 -104
- package/src/cli/auto-setup.ts +1 -9
- package/src/cli/bin.ts +23 -2
- package/src/cli/commands/export.ts +130 -0
- package/src/cli/commands/reindex.ts +107 -0
- package/src/cli/commands/serve.ts +54 -0
- package/src/cli/commands/status.ts +158 -0
- package/src/code-intelligence/indexer.ts +315 -0
- package/src/code-intelligence/linker.ts +178 -0
- package/src/code-intelligence/parser.ts +484 -0
- package/src/code-intelligence/query.ts +291 -0
- package/src/code-intelligence/schema.ts +83 -0
- package/src/code-intelligence/types.ts +95 -0
- package/src/config/defaults.ts +3 -3
- package/src/config/loader.ts +6 -0
- package/src/config/schema.ts +28 -2
- package/src/health/index.ts +5 -2
- package/src/hooks/brain-hook.ts +4 -1
- package/src/hooks/context-hook.ts +69 -10
- package/src/hooks/installer.ts +4 -7
- package/src/intelligence/cross-project/index.ts +1 -7
- package/src/intelligence/prediction/index.ts +1 -7
- package/src/intelligence/reasoning/index.ts +1 -7
- package/src/memory/compression.ts +105 -0
- package/src/memory/fts5-search.ts +456 -0
- package/src/memory/index.ts +342 -38
- package/src/memory/migrations/add-fts5.ts +98 -0
- package/src/memory/pruning.ts +60 -0
- package/src/routing/intent-classifier.ts +58 -1
- package/src/routing/response-filter.ts +128 -0
- package/src/routing/router.ts +457 -54
- package/src/server/http-api.ts +319 -1
- package/src/server/providers/resources.ts +1 -42
- package/src/server/services.ts +113 -12
- package/src/server/web-viewer.ts +1115 -0
- package/src/setup/index.ts +12 -22
- package/src/tools/schemas.ts +1 -1
- package/src/intelligence/cross-project/affinity.ts +0 -159
- package/src/intelligence/cross-project/transfer.ts +0 -201
- package/src/intelligence/prediction/context-anticipator.ts +0 -198
- package/src/intelligence/prediction/decision-predictor.ts +0 -184
- package/src/intelligence/reasoning/counterfactual.ts +0 -248
- package/src/intelligence/reasoning/synthesizer.ts +0 -167
- package/src/setup/wizard.ts +0 -459
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Decision Predictor
|
|
3
|
-
* Predicts likely next decisions based on historical patterns
|
|
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
|
-
|
|
10
|
-
export interface PredictedDecision {
|
|
11
|
-
prediction: string
|
|
12
|
-
confidence: number
|
|
13
|
-
basedOn: Array<{
|
|
14
|
-
id: string
|
|
15
|
-
decision: string
|
|
16
|
-
similarity: number
|
|
17
|
-
}>
|
|
18
|
-
reasoning: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class DecisionPredictor {
|
|
22
|
-
private logger: Logger
|
|
23
|
-
private collections: CollectionManager
|
|
24
|
-
private embeddings?: EmbeddingProvider
|
|
25
|
-
|
|
26
|
-
constructor(logger: Logger, collections: CollectionManager, embeddings?: EmbeddingProvider) {
|
|
27
|
-
this.logger = logger.child({ component: 'decision-predictor' })
|
|
28
|
-
this.collections = collections
|
|
29
|
-
this.embeddings = embeddings
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Predict likely decisions for a given context
|
|
34
|
-
*/
|
|
35
|
-
async predict(context: string, options: {
|
|
36
|
-
project?: string
|
|
37
|
-
limit?: number
|
|
38
|
-
} = {}): Promise<PredictedDecision[]> {
|
|
39
|
-
const { project, limit = 5 } = options
|
|
40
|
-
|
|
41
|
-
// Find similar past contexts and their resulting decisions
|
|
42
|
-
const pastDecisions = await this.findSimilarContextDecisions(context, project, limit * 3)
|
|
43
|
-
|
|
44
|
-
if (pastDecisions.length === 0) {
|
|
45
|
-
return []
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Group by similar decisions
|
|
49
|
-
const grouped = this.groupSimilarDecisions(pastDecisions)
|
|
50
|
-
|
|
51
|
-
// Rank by frequency and similarity
|
|
52
|
-
const predictions = grouped
|
|
53
|
-
.map(group => this.buildPrediction(group, context))
|
|
54
|
-
.sort((a, b) => b.confidence - a.confidence)
|
|
55
|
-
.slice(0, limit)
|
|
56
|
-
|
|
57
|
-
return predictions
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
private async findSimilarContextDecisions(
|
|
61
|
-
context: string,
|
|
62
|
-
project?: string,
|
|
63
|
-
limit: number = 15
|
|
64
|
-
): Promise<Array<{ id: string; decision: string; context: string; similarity: number }>> {
|
|
65
|
-
try {
|
|
66
|
-
const collection = await this.collections.getDecisions()
|
|
67
|
-
|
|
68
|
-
const where: any = project ? { project: { $eq: project } } : undefined
|
|
69
|
-
|
|
70
|
-
let results: any
|
|
71
|
-
|
|
72
|
-
if (this.embeddings) {
|
|
73
|
-
const embedding = await this.embeddings.generate(context)
|
|
74
|
-
results = await collection.query({
|
|
75
|
-
queryEmbeddings: [embedding],
|
|
76
|
-
nResults: limit,
|
|
77
|
-
where,
|
|
78
|
-
include: ['documents', 'metadatas', 'distances']
|
|
79
|
-
})
|
|
80
|
-
} else {
|
|
81
|
-
results = await collection.query({
|
|
82
|
-
queryTexts: [context],
|
|
83
|
-
nResults: limit,
|
|
84
|
-
where,
|
|
85
|
-
include: ['documents', 'metadatas', 'distances']
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!results.ids || !results.ids[0]) return []
|
|
90
|
-
|
|
91
|
-
const processed: Array<{ id: string; decision: string; context: string; similarity: number }> = []
|
|
92
|
-
const ids = results.ids[0]
|
|
93
|
-
const documents = results.documents?.[0] || []
|
|
94
|
-
const metadatas = results.metadatas?.[0] || []
|
|
95
|
-
const distances = results.distances?.[0] || []
|
|
96
|
-
|
|
97
|
-
for (let i = 0; i < ids.length; i++) {
|
|
98
|
-
const similarity = 1 - (distances[i] || 0)
|
|
99
|
-
if (similarity < 0.3) continue
|
|
100
|
-
|
|
101
|
-
processed.push({
|
|
102
|
-
id: ids[i],
|
|
103
|
-
decision: documents[i] || '',
|
|
104
|
-
context: (metadatas[i] as any)?.context || '',
|
|
105
|
-
similarity
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return processed
|
|
110
|
-
} catch (error) {
|
|
111
|
-
this.logger.warn({ error }, 'Failed to find similar contexts for prediction')
|
|
112
|
-
return []
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private groupSimilarDecisions(
|
|
117
|
-
decisions: Array<{ id: string; decision: string; context: string; similarity: number }>
|
|
118
|
-
): Array<Array<{ id: string; decision: string; context: string; similarity: number }>> {
|
|
119
|
-
const groups: Array<Array<{ id: string; decision: string; context: string; similarity: number }>> = []
|
|
120
|
-
|
|
121
|
-
for (const decision of decisions) {
|
|
122
|
-
let addedToGroup = false
|
|
123
|
-
|
|
124
|
-
for (const group of groups) {
|
|
125
|
-
// Check if this decision is similar to the group leader
|
|
126
|
-
const leader = group[0]!
|
|
127
|
-
const jaccardSim = this.jaccardSimilarity(
|
|
128
|
-
leader.decision.toLowerCase(),
|
|
129
|
-
decision.decision.toLowerCase()
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
if (jaccardSim > 0.4) {
|
|
133
|
-
group.push(decision)
|
|
134
|
-
addedToGroup = true
|
|
135
|
-
break
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (!addedToGroup) {
|
|
140
|
-
groups.push([decision])
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return groups.sort((a, b) => b.length - a.length)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private buildPrediction(
|
|
148
|
-
group: Array<{ id: string; decision: string; context: string; similarity: number }>,
|
|
149
|
-
_queryContext: string
|
|
150
|
-
): PredictedDecision {
|
|
151
|
-
// Use the most similar decision as the prediction
|
|
152
|
-
const best = group.reduce((a, b) => a.similarity > b.similarity ? a : b)
|
|
153
|
-
|
|
154
|
-
// Confidence based on group size + similarity
|
|
155
|
-
const groupSizeFactor = Math.min(group.length / 3, 1) // Saturates at 3
|
|
156
|
-
const similarityFactor = best.similarity
|
|
157
|
-
const confidence = (groupSizeFactor * 0.4) + (similarityFactor * 0.6)
|
|
158
|
-
|
|
159
|
-
const basedOn = group.slice(0, 3).map(d => ({
|
|
160
|
-
id: d.id,
|
|
161
|
-
decision: d.decision.slice(0, 150),
|
|
162
|
-
similarity: d.similarity
|
|
163
|
-
}))
|
|
164
|
-
|
|
165
|
-
const reasoning = group.length > 1
|
|
166
|
-
? `Based on ${group.length} similar past decisions with avg ${Math.round(group.reduce((s, d) => s + d.similarity, 0) / group.length * 100)}% match`
|
|
167
|
-
: `Based on a similar past decision with ${Math.round(best.similarity * 100)}% match`
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
prediction: best.decision,
|
|
171
|
-
confidence,
|
|
172
|
-
basedOn,
|
|
173
|
-
reasoning
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private jaccardSimilarity(a: string, b: string): number {
|
|
178
|
-
const setA = new Set(a.split(/\s+/))
|
|
179
|
-
const setB = new Set(b.split(/\s+/))
|
|
180
|
-
const intersection = new Set([...setA].filter(x => setB.has(x)))
|
|
181
|
-
const union = new Set([...setA, ...setB])
|
|
182
|
-
return union.size > 0 ? intersection.size / union.size : 0
|
|
183
|
-
}
|
|
184
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Counterfactual Analysis
|
|
3
|
-
* What-if analysis using knowledge graph and decision history
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Logger } from 'pino'
|
|
7
|
-
import type { InMemoryKnowledgeGraph } from '@/knowledge/graph/memory-graph'
|
|
8
|
-
import type { CollectionManager } from '@/memory/chroma/collection-manager'
|
|
9
|
-
import type { EmbeddingProvider } from '@/memory/chroma/embeddings'
|
|
10
|
-
|
|
11
|
-
export interface WhatIfScenario {
|
|
12
|
-
change: string
|
|
13
|
-
affectedDecisions: Array<{
|
|
14
|
-
id: string
|
|
15
|
-
decision: string
|
|
16
|
-
impact: 'high' | 'medium' | 'low'
|
|
17
|
-
reason: string
|
|
18
|
-
}>
|
|
19
|
-
affectedTechnologies: string[]
|
|
20
|
-
riskLevel: 'high' | 'medium' | 'low'
|
|
21
|
-
summary: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class CounterfactualAnalyzer {
|
|
25
|
-
private logger: Logger
|
|
26
|
-
private graph: InMemoryKnowledgeGraph | null
|
|
27
|
-
private collections: CollectionManager
|
|
28
|
-
private embeddings?: EmbeddingProvider
|
|
29
|
-
|
|
30
|
-
constructor(
|
|
31
|
-
logger: Logger,
|
|
32
|
-
graph: InMemoryKnowledgeGraph | null,
|
|
33
|
-
collections: CollectionManager,
|
|
34
|
-
embeddings?: EmbeddingProvider
|
|
35
|
-
) {
|
|
36
|
-
this.logger = logger.child({ component: 'counterfactual' })
|
|
37
|
-
this.graph = graph
|
|
38
|
-
this.collections = collections
|
|
39
|
-
this.embeddings = embeddings
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Analyze what would happen if a technology/decision changed
|
|
44
|
-
*/
|
|
45
|
-
async whatIf(change: string, options: {
|
|
46
|
-
project?: string
|
|
47
|
-
maxResults?: number
|
|
48
|
-
} = {}): Promise<WhatIfScenario> {
|
|
49
|
-
const { project, maxResults = 20 } = options
|
|
50
|
-
|
|
51
|
-
// Find decisions related to the change
|
|
52
|
-
const relatedDecisions = await this.findRelatedDecisions(change, project, maxResults)
|
|
53
|
-
|
|
54
|
-
// Find affected technologies via knowledge graph
|
|
55
|
-
const affectedTechnologies = this.findAffectedTechnologies(change)
|
|
56
|
-
|
|
57
|
-
// Assess impact on each decision
|
|
58
|
-
const affectedDecisions = relatedDecisions.map(d => ({
|
|
59
|
-
id: d.id,
|
|
60
|
-
decision: d.content.slice(0, 200),
|
|
61
|
-
impact: this.assessImpact(d, change),
|
|
62
|
-
reason: this.explainImpact(d, change)
|
|
63
|
-
}))
|
|
64
|
-
|
|
65
|
-
// Determine overall risk
|
|
66
|
-
const riskLevel = this.calculateRisk(affectedDecisions)
|
|
67
|
-
|
|
68
|
-
// Build summary
|
|
69
|
-
const summary = this.buildSummary(change, affectedDecisions, affectedTechnologies, riskLevel)
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
change,
|
|
73
|
-
affectedDecisions,
|
|
74
|
-
affectedTechnologies,
|
|
75
|
-
riskLevel,
|
|
76
|
-
summary
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private async findRelatedDecisions(
|
|
81
|
-
change: string,
|
|
82
|
-
project?: string,
|
|
83
|
-
limit: number = 20
|
|
84
|
-
): Promise<Array<{ id: string; content: string; metadata: Record<string, any>; similarity: number }>> {
|
|
85
|
-
try {
|
|
86
|
-
const collection = await this.collections.getDecisions()
|
|
87
|
-
|
|
88
|
-
const where: any = project ? { project: { $eq: project } } : undefined
|
|
89
|
-
|
|
90
|
-
let results: any
|
|
91
|
-
|
|
92
|
-
if (this.embeddings) {
|
|
93
|
-
const embedding = await this.embeddings.generate(change)
|
|
94
|
-
results = await collection.query({
|
|
95
|
-
queryEmbeddings: [embedding],
|
|
96
|
-
nResults: limit,
|
|
97
|
-
where,
|
|
98
|
-
include: ['documents', 'metadatas', 'distances']
|
|
99
|
-
})
|
|
100
|
-
} else {
|
|
101
|
-
results = await collection.query({
|
|
102
|
-
queryTexts: [change],
|
|
103
|
-
nResults: limit,
|
|
104
|
-
where,
|
|
105
|
-
include: ['documents', 'metadatas', 'distances']
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (!results.ids || !results.ids[0]) return []
|
|
110
|
-
|
|
111
|
-
const processed: Array<{ id: string; content: string; metadata: Record<string, any>; similarity: number }> = []
|
|
112
|
-
const ids = results.ids[0]
|
|
113
|
-
const documents = results.documents?.[0] || []
|
|
114
|
-
const metadatas = results.metadatas?.[0] || []
|
|
115
|
-
const distances = results.distances?.[0] || []
|
|
116
|
-
|
|
117
|
-
for (let i = 0; i < ids.length; i++) {
|
|
118
|
-
const similarity = 1 - (distances[i] || 0)
|
|
119
|
-
if (similarity < 0.2) continue
|
|
120
|
-
|
|
121
|
-
processed.push({
|
|
122
|
-
id: ids[i],
|
|
123
|
-
content: documents[i] || '',
|
|
124
|
-
metadata: metadatas[i] || {},
|
|
125
|
-
similarity
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return processed
|
|
130
|
-
} catch (error) {
|
|
131
|
-
this.logger.warn({ error, change }, 'Failed to find related decisions for what-if')
|
|
132
|
-
return []
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private findAffectedTechnologies(change: string): string[] {
|
|
137
|
-
if (!this.graph) return []
|
|
138
|
-
|
|
139
|
-
const technologies: string[] = []
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
// Find nodes matching the change description
|
|
143
|
-
const changeWords = change.toLowerCase().split(/\s+/)
|
|
144
|
-
|
|
145
|
-
const matchingNodes = this.graph.findNodes({ type: 'technology' })
|
|
146
|
-
.filter(node => {
|
|
147
|
-
const name = node.name.toLowerCase()
|
|
148
|
-
return changeWords.some(w => name.includes(w) || w.includes(name))
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
for (const node of matchingNodes) {
|
|
152
|
-
technologies.push(node.name)
|
|
153
|
-
|
|
154
|
-
// Traverse to find connected technologies
|
|
155
|
-
const connected = this.graph.traverse(node.id, {
|
|
156
|
-
maxDepth: 2,
|
|
157
|
-
nodeTypes: ['technology']
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
for (const connNode of connected) {
|
|
161
|
-
if (connNode.id !== node.id && !technologies.includes(connNode.name)) {
|
|
162
|
-
technologies.push(connNode.name)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} catch (error) {
|
|
167
|
-
this.logger.debug({ error }, 'Failed to traverse knowledge graph')
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return technologies.slice(0, 20)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private assessImpact(
|
|
174
|
-
decision: { content: string; metadata: Record<string, any>; similarity: number },
|
|
175
|
-
change: string
|
|
176
|
-
): 'high' | 'medium' | 'low' {
|
|
177
|
-
// High similarity = high impact
|
|
178
|
-
if (decision.similarity > 0.7) return 'high'
|
|
179
|
-
if (decision.similarity > 0.5) return 'medium'
|
|
180
|
-
|
|
181
|
-
// Check if the decision directly references the change topic
|
|
182
|
-
const changeTerms = change.toLowerCase().split(/\s+/)
|
|
183
|
-
const content = decision.content.toLowerCase()
|
|
184
|
-
const directMentions = changeTerms.filter(t => t.length > 3 && content.includes(t)).length
|
|
185
|
-
|
|
186
|
-
if (directMentions >= 3) return 'high'
|
|
187
|
-
if (directMentions >= 1) return 'medium'
|
|
188
|
-
|
|
189
|
-
return 'low'
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private explainImpact(
|
|
193
|
-
decision: { content: string; metadata: Record<string, any> },
|
|
194
|
-
_change: string
|
|
195
|
-
): string {
|
|
196
|
-
const reasoning = decision.metadata.reasoning || ''
|
|
197
|
-
const context = decision.metadata.context || ''
|
|
198
|
-
|
|
199
|
-
if (reasoning) {
|
|
200
|
-
return `This decision was based on: "${reasoning.slice(0, 100)}..." which may need revision.`
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (context) {
|
|
204
|
-
return `Made in context: "${context.slice(0, 100)}..." which would be affected.`
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return 'This decision may need to be reconsidered.'
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
private calculateRisk(
|
|
211
|
-
affectedDecisions: Array<{ impact: 'high' | 'medium' | 'low' }>
|
|
212
|
-
): 'high' | 'medium' | 'low' {
|
|
213
|
-
const highCount = affectedDecisions.filter(d => d.impact === 'high').length
|
|
214
|
-
const medCount = affectedDecisions.filter(d => d.impact === 'medium').length
|
|
215
|
-
|
|
216
|
-
if (highCount >= 3) return 'high'
|
|
217
|
-
if (highCount >= 1 || medCount >= 5) return 'medium'
|
|
218
|
-
return 'low'
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private buildSummary(
|
|
222
|
-
change: string,
|
|
223
|
-
affectedDecisions: Array<{ impact: string }>,
|
|
224
|
-
affectedTechnologies: string[],
|
|
225
|
-
riskLevel: string
|
|
226
|
-
): string {
|
|
227
|
-
const parts: string[] = []
|
|
228
|
-
|
|
229
|
-
parts.push(`## What-If Analysis: "${change}"`)
|
|
230
|
-
parts.push('')
|
|
231
|
-
parts.push(`**Risk Level:** ${riskLevel.toUpperCase()}`)
|
|
232
|
-
parts.push(`**Affected Decisions:** ${affectedDecisions.length}`)
|
|
233
|
-
|
|
234
|
-
const highImpact = affectedDecisions.filter(d => d.impact === 'high').length
|
|
235
|
-
const medImpact = affectedDecisions.filter(d => d.impact === 'medium').length
|
|
236
|
-
const lowImpact = affectedDecisions.filter(d => d.impact === 'low').length
|
|
237
|
-
|
|
238
|
-
parts.push(` - High impact: ${highImpact}`)
|
|
239
|
-
parts.push(` - Medium impact: ${medImpact}`)
|
|
240
|
-
parts.push(` - Low impact: ${lowImpact}`)
|
|
241
|
-
|
|
242
|
-
if (affectedTechnologies.length > 0) {
|
|
243
|
-
parts.push(`\n**Affected Technologies:** ${affectedTechnologies.join(', ')}`)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return parts.join('\n')
|
|
247
|
-
}
|
|
248
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Result Synthesizer
|
|
3
|
-
* Combines multiple retrieved sources into coherent answers using heuristics
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Logger } from 'pino'
|
|
7
|
-
import type { ChainResult } from './chain-retrieval'
|
|
8
|
-
|
|
9
|
-
export interface SynthesizedResult {
|
|
10
|
-
summary: string
|
|
11
|
-
sources: Array<{
|
|
12
|
-
id: string
|
|
13
|
-
relevance: number
|
|
14
|
-
excerpt: string
|
|
15
|
-
}>
|
|
16
|
-
confidence: number
|
|
17
|
-
themes: string[]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class ResultSynthesizer {
|
|
21
|
-
constructor(_logger: Logger) {
|
|
22
|
-
// Logger available for future use
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Synthesize multiple sources into a coherent answer
|
|
27
|
-
*/
|
|
28
|
-
synthesize(query: string, results: ChainResult[]): SynthesizedResult {
|
|
29
|
-
if (results.length === 0) {
|
|
30
|
-
return {
|
|
31
|
-
summary: 'No relevant information found.',
|
|
32
|
-
sources: [],
|
|
33
|
-
confidence: 0,
|
|
34
|
-
themes: []
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Sort by relevance
|
|
39
|
-
const sorted = [...results].sort((a, b) => b.similarity - a.similarity)
|
|
40
|
-
|
|
41
|
-
// Extract themes
|
|
42
|
-
const themes = this.extractThemes(sorted)
|
|
43
|
-
|
|
44
|
-
// Build summary from top results
|
|
45
|
-
const summary = this.buildSummary(query, sorted, themes)
|
|
46
|
-
|
|
47
|
-
// Calculate confidence based on result quality
|
|
48
|
-
const confidence = this.calculateConfidence(sorted)
|
|
49
|
-
|
|
50
|
-
// Build source list
|
|
51
|
-
const sources = sorted.slice(0, 10).map(r => ({
|
|
52
|
-
id: r.id,
|
|
53
|
-
relevance: r.similarity,
|
|
54
|
-
excerpt: r.content.slice(0, 200)
|
|
55
|
-
}))
|
|
56
|
-
|
|
57
|
-
return {
|
|
58
|
-
summary,
|
|
59
|
-
sources,
|
|
60
|
-
confidence,
|
|
61
|
-
themes
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private extractThemes(results: ChainResult[]): string[] {
|
|
66
|
-
const termFreq = new Map<string, number>()
|
|
67
|
-
|
|
68
|
-
for (const result of results) {
|
|
69
|
-
const words = result.content
|
|
70
|
-
.toLowerCase()
|
|
71
|
-
.replace(/[^a-z0-9\s]/g, ' ')
|
|
72
|
-
.split(/\s+/)
|
|
73
|
-
.filter(w => w.length > 3 && !this.isCommonWord(w))
|
|
74
|
-
|
|
75
|
-
const seen = new Set<string>()
|
|
76
|
-
for (const word of words) {
|
|
77
|
-
if (seen.has(word)) continue
|
|
78
|
-
seen.add(word)
|
|
79
|
-
termFreq.set(word, (termFreq.get(word) || 0) + 1)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Return terms appearing in multiple results
|
|
84
|
-
return Array.from(termFreq.entries())
|
|
85
|
-
.filter(([_, count]) => count >= Math.max(2, results.length * 0.3))
|
|
86
|
-
.sort((a, b) => b[1] - a[1])
|
|
87
|
-
.slice(0, 10)
|
|
88
|
-
.map(([term]) => term)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private buildSummary(_query: string, results: ChainResult[], themes: string[]): string {
|
|
92
|
-
const parts: string[] = []
|
|
93
|
-
|
|
94
|
-
// Group by type
|
|
95
|
-
const decisions = results.filter(r => r.metadata.type === 'decision' || r.metadata.source === 'remember_decision')
|
|
96
|
-
const patterns = results.filter(r => r.metadata.type === 'pattern' || r.metadata.source === 'recognize_pattern')
|
|
97
|
-
const corrections = results.filter(r => r.metadata.type === 'correction' || r.metadata.source === 'record_correction')
|
|
98
|
-
const other = results.filter(r =>
|
|
99
|
-
!decisions.includes(r) && !patterns.includes(r) && !corrections.includes(r)
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
if (decisions.length > 0) {
|
|
103
|
-
parts.push(`**Decisions (${decisions.length}):**`)
|
|
104
|
-
for (const d of decisions.slice(0, 3)) {
|
|
105
|
-
parts.push(`- ${d.content.slice(0, 150)}`)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (patterns.length > 0) {
|
|
110
|
-
parts.push(`\n**Patterns (${patterns.length}):**`)
|
|
111
|
-
for (const p of patterns.slice(0, 3)) {
|
|
112
|
-
parts.push(`- ${p.content.slice(0, 150)}`)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (corrections.length > 0) {
|
|
117
|
-
parts.push(`\n**Corrections (${corrections.length}):**`)
|
|
118
|
-
for (const c of corrections.slice(0, 3)) {
|
|
119
|
-
parts.push(`- ${c.content.slice(0, 150)}`)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (other.length > 0) {
|
|
124
|
-
parts.push(`\n**Other (${other.length}):**`)
|
|
125
|
-
for (const o of other.slice(0, 3)) {
|
|
126
|
-
parts.push(`- ${o.content.slice(0, 150)}`)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (themes.length > 0) {
|
|
131
|
-
parts.push(`\n**Key themes:** ${themes.join(', ')}`)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return parts.join('\n')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private calculateConfidence(results: ChainResult[]): number {
|
|
138
|
-
if (results.length === 0) return 0
|
|
139
|
-
|
|
140
|
-
// Average similarity of top results
|
|
141
|
-
const top = results.slice(0, 5)
|
|
142
|
-
const avgSimilarity = top.reduce((sum, r) => sum + r.similarity, 0) / top.length
|
|
143
|
-
|
|
144
|
-
// Factor in number of results
|
|
145
|
-
const countFactor = Math.min(results.length / 5, 1) // More results = higher confidence, up to 5
|
|
146
|
-
|
|
147
|
-
return Math.min(avgSimilarity * countFactor, 1)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private isCommonWord(word: string): boolean {
|
|
151
|
-
const common = new Set([
|
|
152
|
-
'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can',
|
|
153
|
-
'was', 'one', 'our', 'out', 'has', 'had', 'been', 'have', 'with',
|
|
154
|
-
'this', 'that', 'from', 'they', 'will', 'would', 'there', 'their',
|
|
155
|
-
'what', 'about', 'which', 'when', 'make', 'like', 'time', 'just',
|
|
156
|
-
'know', 'take', 'into', 'year', 'your', 'good', 'some', 'could',
|
|
157
|
-
'them', 'than', 'other', 'then', 'also', 'back', 'after', 'work',
|
|
158
|
-
'first', 'well', 'even', 'most', 'find', 'here', 'thing', 'many',
|
|
159
|
-
'should', 'because', 'does', 'each', 'much', 'before', 'must',
|
|
160
|
-
'through', 'being', 'using', 'used', 'decision', 'decided',
|
|
161
|
-
'recommend', 'instead', 'project', 'context', 'reasoning', 'pattern',
|
|
162
|
-
'correction', 'original', 'still', 'between', 'think', 'over',
|
|
163
|
-
'come', 'only', 'give', 'look', 'people'
|
|
164
|
-
])
|
|
165
|
-
return common.has(word)
|
|
166
|
-
}
|
|
167
|
-
}
|