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
package/src/setup/index.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import { createLogger } from '@/utils/logger'
|
|
2
|
-
import { resolveHomePath } from '@/config/home'
|
|
3
1
|
import { ensureHomeDirectory } from '@/cli/auto-setup'
|
|
4
|
-
import {
|
|
5
|
-
import { renderBanner, typewrite, transition, box, errorText, theme } from '@/cli/ui/index.js'
|
|
2
|
+
import { renderBanner, theme } from '@/cli/ui/index.js'
|
|
6
3
|
import { readFileSync } from 'node:fs'
|
|
7
4
|
import { resolve, dirname } from 'node:path'
|
|
8
5
|
import { fileURLToPath } from 'node:url'
|
|
9
6
|
|
|
10
|
-
export * from './wizard'
|
|
11
|
-
|
|
12
7
|
const __filename = fileURLToPath(import.meta.url)
|
|
13
8
|
const __dirname = dirname(__filename)
|
|
14
9
|
const PACKAGE_ROOT = resolve(__dirname, '..', '..')
|
|
@@ -22,27 +17,22 @@ function getVersion(): string {
|
|
|
22
17
|
}
|
|
23
18
|
}
|
|
24
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Phase 30: Zero-config setup — no wizard, no prompts.
|
|
22
|
+
* Just ensures the home directory exists with sensible defaults.
|
|
23
|
+
*/
|
|
25
24
|
export async function runSetup() {
|
|
26
|
-
ensureHomeDirectory()
|
|
27
|
-
|
|
28
25
|
const version = getVersion()
|
|
29
26
|
console.log()
|
|
30
27
|
console.log(renderBanner(version))
|
|
31
28
|
console.log()
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
await transition(400)
|
|
35
|
-
|
|
36
|
-
const logger = createLogger('info', resolveHomePath('./logs/setup.log'))
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const wizard = new SetupWizard(logger)
|
|
40
|
-
const answers = await wizard.run()
|
|
41
|
-
await wizard.applyConfiguration(answers)
|
|
30
|
+
ensureHomeDirectory()
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
32
|
+
console.log()
|
|
33
|
+
console.log(theme.bold('Setup complete! No configuration needed.'))
|
|
34
|
+
console.log()
|
|
35
|
+
console.log(` ${theme.primary('Next:')} ${theme.dim('claude-brain start')}`)
|
|
36
|
+
console.log(` ${theme.dim('ChromaDB is optional — SQLite FTS5 works out of the box.')}`)
|
|
37
|
+
console.log()
|
|
48
38
|
}
|
package/src/tools/schemas.ts
CHANGED
|
@@ -590,7 +590,7 @@ export const TOOLS = {
|
|
|
590
590
|
*/
|
|
591
591
|
BRAIN: {
|
|
592
592
|
name: 'brain',
|
|
593
|
-
description: 'Your persistent memory. Tell it decisions, ask it questions, or update/delete past notes. Most context is captured automatically — only call this for intentional storage or recall.',
|
|
593
|
+
description: 'Your persistent memory. Tell it decisions, ask it questions, or update/delete past notes. Most context is captured automatically — only call this for intentional storage or recall. Search returns compact summaries — use brain("details {ID}") for full context.',
|
|
594
594
|
inputSchema: {
|
|
595
595
|
type: 'object' as const,
|
|
596
596
|
properties: {
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project Affinity
|
|
3
|
-
* Computes similarity between projects based on decisions, patterns, and tech stack
|
|
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 ProjectAffinity {
|
|
11
|
-
projectA: string
|
|
12
|
-
projectB: string
|
|
13
|
-
similarity: number
|
|
14
|
-
sharedTopics: string[]
|
|
15
|
-
sharedTechnologies: string[]
|
|
16
|
-
decisionOverlap: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class AffinityCalculator {
|
|
20
|
-
private logger: Logger
|
|
21
|
-
private collections: CollectionManager
|
|
22
|
-
constructor(logger: Logger, collections: CollectionManager, _embeddings?: EmbeddingProvider) {
|
|
23
|
-
this.logger = logger.child({ component: 'affinity-calculator' })
|
|
24
|
-
this.collections = collections
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Calculate affinity between all pairs of projects
|
|
29
|
-
*/
|
|
30
|
-
async calculateAffinities(options: {
|
|
31
|
-
minDecisions?: number
|
|
32
|
-
} = {}): Promise<ProjectAffinity[]> {
|
|
33
|
-
const { minDecisions = 2 } = options
|
|
34
|
-
|
|
35
|
-
// Get all decisions grouped by project
|
|
36
|
-
const byProject = await this.getDecisionsByProject()
|
|
37
|
-
|
|
38
|
-
// Filter out projects with too few decisions
|
|
39
|
-
const projects = Array.from(byProject.entries())
|
|
40
|
-
.filter(([_, decisions]) => decisions.length >= minDecisions)
|
|
41
|
-
|
|
42
|
-
const affinities: ProjectAffinity[] = []
|
|
43
|
-
|
|
44
|
-
// Compare all pairs
|
|
45
|
-
for (let i = 0; i < projects.length; i++) {
|
|
46
|
-
for (let j = i + 1; j < projects.length; j++) {
|
|
47
|
-
const [projectA, decisionsA] = projects[i]!
|
|
48
|
-
const [projectB, decisionsB] = projects[j]!
|
|
49
|
-
|
|
50
|
-
const affinity = this.compareProjects(projectA, decisionsA, projectB, decisionsB)
|
|
51
|
-
if (affinity.similarity > 0.1) {
|
|
52
|
-
affinities.push(affinity)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return affinities.sort((a, b) => b.similarity - a.similarity)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Find most similar projects to a given project
|
|
62
|
-
*/
|
|
63
|
-
async findSimilarProjects(project: string, limit: number = 5): Promise<ProjectAffinity[]> {
|
|
64
|
-
const affinities = await this.calculateAffinities()
|
|
65
|
-
|
|
66
|
-
return affinities
|
|
67
|
-
.filter(a => a.projectA === project || a.projectB === project)
|
|
68
|
-
.sort((a, b) => b.similarity - a.similarity)
|
|
69
|
-
.slice(0, limit)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private async getDecisionsByProject(): Promise<Map<string, Array<{ id: string; content: string; tags: string[] }>>> {
|
|
73
|
-
try {
|
|
74
|
-
const collection = await this.collections.getDecisions()
|
|
75
|
-
|
|
76
|
-
const results = await collection.get({
|
|
77
|
-
include: ['documents', 'metadatas']
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
const byProject = new Map<string, Array<{ id: string; content: string; tags: string[] }>>()
|
|
81
|
-
|
|
82
|
-
for (let i = 0; i < results.ids.length; i++) {
|
|
83
|
-
const project = (results.metadatas![i] as any)?.project || ''
|
|
84
|
-
if (!project) continue
|
|
85
|
-
|
|
86
|
-
if (!byProject.has(project)) byProject.set(project, [])
|
|
87
|
-
|
|
88
|
-
byProject.get(project)!.push({
|
|
89
|
-
id: results.ids[i]!,
|
|
90
|
-
content: results.documents![i] as string || '',
|
|
91
|
-
tags: String((results.metadatas![i] as any)?.tags || '').split(',').filter(t => t.trim())
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return byProject
|
|
96
|
-
} catch (error) {
|
|
97
|
-
this.logger.warn({ error }, 'Failed to get decisions by project')
|
|
98
|
-
return new Map()
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private compareProjects(
|
|
103
|
-
projectA: string,
|
|
104
|
-
decisionsA: Array<{ id: string; content: string; tags: string[] }>,
|
|
105
|
-
projectB: string,
|
|
106
|
-
decisionsB: Array<{ id: string; content: string; tags: string[] }>
|
|
107
|
-
): ProjectAffinity {
|
|
108
|
-
// Term-based similarity
|
|
109
|
-
const termsA = this.extractAllTerms(decisionsA)
|
|
110
|
-
const termsB = this.extractAllTerms(decisionsB)
|
|
111
|
-
|
|
112
|
-
const sharedTopics = Array.from(termsA).filter(t => termsB.has(t))
|
|
113
|
-
const allTopics = new Set([...termsA, ...termsB])
|
|
114
|
-
|
|
115
|
-
const topicSimilarity = allTopics.size > 0 ? sharedTopics.length / allTopics.size : 0
|
|
116
|
-
|
|
117
|
-
// Tag-based similarity
|
|
118
|
-
const tagsA = new Set(decisionsA.flatMap(d => d.tags))
|
|
119
|
-
const tagsB = new Set(decisionsB.flatMap(d => d.tags))
|
|
120
|
-
|
|
121
|
-
const sharedTags = Array.from(tagsA).filter(t => tagsB.has(t))
|
|
122
|
-
const allTags = new Set([...tagsA, ...tagsB])
|
|
123
|
-
const tagSimilarity = allTags.size > 0 ? sharedTags.length / allTags.size : 0
|
|
124
|
-
|
|
125
|
-
// Combined similarity
|
|
126
|
-
const similarity = topicSimilarity * 0.7 + tagSimilarity * 0.3
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
projectA,
|
|
130
|
-
projectB,
|
|
131
|
-
similarity,
|
|
132
|
-
sharedTopics: sharedTopics.slice(0, 20),
|
|
133
|
-
sharedTechnologies: sharedTags.slice(0, 10),
|
|
134
|
-
decisionOverlap: sharedTopics.length
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private extractAllTerms(decisions: Array<{ content: string }>): Set<string> {
|
|
139
|
-
const terms = new Set<string>()
|
|
140
|
-
const stopWords = new Set([
|
|
141
|
-
'the', 'and', 'for', 'are', 'but', 'not', 'all', 'can', 'was',
|
|
142
|
-
'has', 'had', 'been', 'have', 'with', 'this', 'that', 'from',
|
|
143
|
-
'use', 'using', 'used', 'will', 'would', 'should', 'also',
|
|
144
|
-
'decision', 'decided', 'recommend', 'instead', 'because',
|
|
145
|
-
'project', 'context', 'reasoning', 'about', 'which', 'when'
|
|
146
|
-
])
|
|
147
|
-
|
|
148
|
-
for (const d of decisions) {
|
|
149
|
-
const words = d.content.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/)
|
|
150
|
-
for (const w of words) {
|
|
151
|
-
if (w.length > 3 && !stopWords.has(w)) {
|
|
152
|
-
terms.add(w)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return terms
|
|
158
|
-
}
|
|
159
|
-
}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Knowledge Transfer
|
|
3
|
-
* Transfer knowledge from one project to another based on similarity
|
|
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 TransferableKnowledge {
|
|
11
|
-
sourceProject: string
|
|
12
|
-
targetProject: string
|
|
13
|
-
items: TransferItem[]
|
|
14
|
-
affinityScore: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface TransferItem {
|
|
18
|
-
type: 'decision' | 'pattern' | 'correction'
|
|
19
|
-
id: string
|
|
20
|
-
content: string
|
|
21
|
-
relevance: number
|
|
22
|
-
reasoning: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class KnowledgeTransfer {
|
|
26
|
-
private logger: Logger
|
|
27
|
-
private collections: CollectionManager
|
|
28
|
-
private embeddings?: EmbeddingProvider
|
|
29
|
-
|
|
30
|
-
constructor(logger: Logger, collections: CollectionManager, embeddings?: EmbeddingProvider) {
|
|
31
|
-
this.logger = logger.child({ component: 'knowledge-transfer' })
|
|
32
|
-
this.collections = collections
|
|
33
|
-
this.embeddings = embeddings
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Find transferable knowledge from source project to apply to target project
|
|
38
|
-
*/
|
|
39
|
-
async findTransferable(sourceProject: string, targetProject: string, options: {
|
|
40
|
-
limit?: number
|
|
41
|
-
minRelevance?: number
|
|
42
|
-
} = {}): Promise<TransferableKnowledge> {
|
|
43
|
-
const { limit = 10, minRelevance = 0.4 } = options
|
|
44
|
-
|
|
45
|
-
// Get target project's decisions to understand its context
|
|
46
|
-
const targetContext = await this.getProjectContext(targetProject)
|
|
47
|
-
|
|
48
|
-
if (!targetContext) {
|
|
49
|
-
return {
|
|
50
|
-
sourceProject,
|
|
51
|
-
targetProject,
|
|
52
|
-
items: [],
|
|
53
|
-
affinityScore: 0
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Search source project for relevant knowledge
|
|
58
|
-
const items: TransferItem[] = []
|
|
59
|
-
|
|
60
|
-
// Decisions
|
|
61
|
-
const decisions = await this.searchSourceProject(
|
|
62
|
-
sourceProject,
|
|
63
|
-
targetContext,
|
|
64
|
-
'decisions',
|
|
65
|
-
limit,
|
|
66
|
-
minRelevance
|
|
67
|
-
)
|
|
68
|
-
items.push(...decisions)
|
|
69
|
-
|
|
70
|
-
// Patterns
|
|
71
|
-
const patterns = await this.searchSourceProject(
|
|
72
|
-
sourceProject,
|
|
73
|
-
targetContext,
|
|
74
|
-
'patterns',
|
|
75
|
-
limit,
|
|
76
|
-
minRelevance
|
|
77
|
-
)
|
|
78
|
-
items.push(...patterns)
|
|
79
|
-
|
|
80
|
-
// Corrections
|
|
81
|
-
const corrections = await this.searchSourceProject(
|
|
82
|
-
sourceProject,
|
|
83
|
-
targetContext,
|
|
84
|
-
'corrections',
|
|
85
|
-
limit,
|
|
86
|
-
minRelevance
|
|
87
|
-
)
|
|
88
|
-
items.push(...corrections)
|
|
89
|
-
|
|
90
|
-
// Sort by relevance
|
|
91
|
-
items.sort((a, b) => b.relevance - a.relevance)
|
|
92
|
-
|
|
93
|
-
// Calculate affinity
|
|
94
|
-
const affinityScore = items.length > 0
|
|
95
|
-
? items.reduce((sum, i) => sum + i.relevance, 0) / items.length
|
|
96
|
-
: 0
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
sourceProject,
|
|
100
|
-
targetProject,
|
|
101
|
-
items: items.slice(0, limit),
|
|
102
|
-
affinityScore
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private async getProjectContext(project: string): Promise<string | null> {
|
|
107
|
-
try {
|
|
108
|
-
const collection = await this.collections.getDecisions()
|
|
109
|
-
|
|
110
|
-
const results = await collection.get({
|
|
111
|
-
where: { project: { $eq: project } },
|
|
112
|
-
include: ['documents'],
|
|
113
|
-
limit: 20
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
if (results.ids.length === 0) return null
|
|
117
|
-
|
|
118
|
-
// Combine recent decisions into a context string
|
|
119
|
-
return results.documents!
|
|
120
|
-
.slice(0, 10)
|
|
121
|
-
.map(d => d as string)
|
|
122
|
-
.join(' ')
|
|
123
|
-
} catch (error) {
|
|
124
|
-
this.logger.debug({ error, project }, 'Failed to get project context')
|
|
125
|
-
return null
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private async searchSourceProject(
|
|
130
|
-
sourceProject: string,
|
|
131
|
-
targetContext: string,
|
|
132
|
-
collectionType: 'decisions' | 'patterns' | 'corrections',
|
|
133
|
-
limit: number,
|
|
134
|
-
minRelevance: number
|
|
135
|
-
): Promise<TransferItem[]> {
|
|
136
|
-
try {
|
|
137
|
-
let collection
|
|
138
|
-
let type: 'decision' | 'pattern' | 'correction'
|
|
139
|
-
|
|
140
|
-
switch (collectionType) {
|
|
141
|
-
case 'decisions':
|
|
142
|
-
collection = await this.collections.getDecisions()
|
|
143
|
-
type = 'decision'
|
|
144
|
-
break
|
|
145
|
-
case 'patterns':
|
|
146
|
-
collection = await this.collections.getPatterns()
|
|
147
|
-
type = 'pattern'
|
|
148
|
-
break
|
|
149
|
-
case 'corrections':
|
|
150
|
-
collection = await this.collections.getCorrections()
|
|
151
|
-
type = 'correction'
|
|
152
|
-
break
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const where: any = { project: { $eq: sourceProject } }
|
|
156
|
-
|
|
157
|
-
let results: any
|
|
158
|
-
|
|
159
|
-
if (this.embeddings) {
|
|
160
|
-
// Use truncated target context for embedding
|
|
161
|
-
const queryText = targetContext.slice(0, 500)
|
|
162
|
-
const embedding = await this.embeddings.generate(queryText)
|
|
163
|
-
results = await collection.query({
|
|
164
|
-
queryEmbeddings: [embedding],
|
|
165
|
-
nResults: limit,
|
|
166
|
-
where,
|
|
167
|
-
include: ['documents', 'metadatas', 'distances']
|
|
168
|
-
})
|
|
169
|
-
} else {
|
|
170
|
-
results = await collection.query({
|
|
171
|
-
queryTexts: [targetContext.slice(0, 500)],
|
|
172
|
-
nResults: limit,
|
|
173
|
-
where,
|
|
174
|
-
include: ['documents', 'metadatas', 'distances']
|
|
175
|
-
})
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!results.ids || !results.ids[0]) return []
|
|
179
|
-
|
|
180
|
-
const items: TransferItem[] = []
|
|
181
|
-
|
|
182
|
-
for (let i = 0; i < results.ids[0].length; i++) {
|
|
183
|
-
const similarity = 1 - (results.distances?.[0]?.[i] || 0)
|
|
184
|
-
if (similarity < minRelevance) continue
|
|
185
|
-
|
|
186
|
-
items.push({
|
|
187
|
-
type,
|
|
188
|
-
id: results.ids[0][i],
|
|
189
|
-
content: results.documents?.[0]?.[i] || '',
|
|
190
|
-
relevance: similarity,
|
|
191
|
-
reasoning: `Relevant ${type} from ${sourceProject} (${Math.round(similarity * 100)}% match)`
|
|
192
|
-
})
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return items
|
|
196
|
-
} catch (error) {
|
|
197
|
-
this.logger.debug({ error, sourceProject, collectionType }, 'Failed to search source project')
|
|
198
|
-
return []
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Context Anticipator
|
|
3
|
-
* Anticipates what context will be needed based on current activity
|
|
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 AnticipatedContext {
|
|
11
|
-
topic: string
|
|
12
|
-
relevance: number
|
|
13
|
-
relatedDecisions: number
|
|
14
|
-
suggestedQueries: string[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class ContextAnticipator {
|
|
18
|
-
private logger: Logger
|
|
19
|
-
private collections: CollectionManager
|
|
20
|
-
private embeddings?: EmbeddingProvider
|
|
21
|
-
private recentTopics: Array<{ topic: string; timestamp: number }> = []
|
|
22
|
-
|
|
23
|
-
constructor(logger: Logger, collections: CollectionManager, embeddings?: EmbeddingProvider) {
|
|
24
|
-
this.logger = logger.child({ component: 'context-anticipator' })
|
|
25
|
-
this.collections = collections
|
|
26
|
-
this.embeddings = embeddings
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Record a topic being worked on for anticipation
|
|
31
|
-
*/
|
|
32
|
-
recordActivity(topic: string): void {
|
|
33
|
-
this.recentTopics.push({ topic, timestamp: Date.now() })
|
|
34
|
-
// Keep last 50 topics
|
|
35
|
-
if (this.recentTopics.length > 50) {
|
|
36
|
-
this.recentTopics = this.recentTopics.slice(-50)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Anticipate what context might be needed next
|
|
42
|
-
*/
|
|
43
|
-
async anticipate(currentTask: string, options: {
|
|
44
|
-
project?: string
|
|
45
|
-
limit?: number
|
|
46
|
-
} = {}): Promise<AnticipatedContext[]> {
|
|
47
|
-
const { project, limit = 5 } = options
|
|
48
|
-
|
|
49
|
-
// Get patterns of what typically follows similar tasks
|
|
50
|
-
const followUpTopics = await this.findFollowUpPatterns(currentTask, project)
|
|
51
|
-
|
|
52
|
-
// Get related but not-yet-accessed topics
|
|
53
|
-
const relatedTopics = await this.findRelatedTopics(currentTask, project)
|
|
54
|
-
|
|
55
|
-
// Merge and rank
|
|
56
|
-
const merged = new Map<string, AnticipatedContext>()
|
|
57
|
-
|
|
58
|
-
for (const t of followUpTopics) {
|
|
59
|
-
merged.set(t.topic, t)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
for (const t of relatedTopics) {
|
|
63
|
-
if (merged.has(t.topic)) {
|
|
64
|
-
const existing = merged.get(t.topic)!
|
|
65
|
-
existing.relevance = Math.max(existing.relevance, t.relevance)
|
|
66
|
-
existing.relatedDecisions = Math.max(existing.relatedDecisions, t.relatedDecisions)
|
|
67
|
-
} else {
|
|
68
|
-
merged.set(t.topic, t)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return Array.from(merged.values())
|
|
73
|
-
.sort((a, b) => b.relevance - a.relevance)
|
|
74
|
-
.slice(0, limit)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private async findFollowUpPatterns(
|
|
78
|
-
currentTask: string,
|
|
79
|
-
project?: string
|
|
80
|
-
): Promise<AnticipatedContext[]> {
|
|
81
|
-
try {
|
|
82
|
-
const collection = await this.collections.getDecisions()
|
|
83
|
-
|
|
84
|
-
const where: any = project ? { project: { $eq: project } } : undefined
|
|
85
|
-
|
|
86
|
-
let results: any
|
|
87
|
-
|
|
88
|
-
if (this.embeddings) {
|
|
89
|
-
const embedding = await this.embeddings.generate(currentTask)
|
|
90
|
-
results = await collection.query({
|
|
91
|
-
queryEmbeddings: [embedding],
|
|
92
|
-
nResults: 10,
|
|
93
|
-
where,
|
|
94
|
-
include: ['documents', 'metadatas', 'distances']
|
|
95
|
-
})
|
|
96
|
-
} else {
|
|
97
|
-
results = await collection.query({
|
|
98
|
-
queryTexts: [currentTask],
|
|
99
|
-
nResults: 10,
|
|
100
|
-
where,
|
|
101
|
-
include: ['documents', 'metadatas', 'distances']
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!results.ids || !results.ids[0]) return []
|
|
106
|
-
|
|
107
|
-
// Extract unique topics from the results
|
|
108
|
-
const topicCounts = new Map<string, { count: number; similarity: number }>()
|
|
109
|
-
const metadatas = results.metadatas?.[0] || []
|
|
110
|
-
const distances = results.distances?.[0] || []
|
|
111
|
-
|
|
112
|
-
for (let i = 0; i < metadatas.length; i++) {
|
|
113
|
-
const tags = String((metadatas[i] as any)?.tags || '').split(',').filter((t: string) => t.trim())
|
|
114
|
-
const similarity = 1 - (distances[i] || 0)
|
|
115
|
-
|
|
116
|
-
for (const tag of tags) {
|
|
117
|
-
const trimmed = tag.trim().toLowerCase()
|
|
118
|
-
if (!trimmed) continue
|
|
119
|
-
|
|
120
|
-
const existing = topicCounts.get(trimmed)
|
|
121
|
-
if (existing) {
|
|
122
|
-
existing.count++
|
|
123
|
-
existing.similarity = Math.max(existing.similarity, similarity)
|
|
124
|
-
} else {
|
|
125
|
-
topicCounts.set(trimmed, { count: 1, similarity })
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return Array.from(topicCounts.entries()).map(([topic, data]) => ({
|
|
131
|
-
topic,
|
|
132
|
-
relevance: data.similarity * (1 + Math.log(data.count + 1) / 5),
|
|
133
|
-
relatedDecisions: data.count,
|
|
134
|
-
suggestedQueries: [`What decisions were made about ${topic}?`, `Show patterns for ${topic}`]
|
|
135
|
-
}))
|
|
136
|
-
} catch (error) {
|
|
137
|
-
this.logger.debug({ error }, 'Failed to find follow-up patterns')
|
|
138
|
-
return []
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private async findRelatedTopics(
|
|
143
|
-
currentTask: string,
|
|
144
|
-
project?: string
|
|
145
|
-
): Promise<AnticipatedContext[]> {
|
|
146
|
-
try {
|
|
147
|
-
const collection = await this.collections.getPatterns()
|
|
148
|
-
|
|
149
|
-
const where: any = project ? { project: { $eq: project } } : undefined
|
|
150
|
-
|
|
151
|
-
let results: any
|
|
152
|
-
|
|
153
|
-
if (this.embeddings) {
|
|
154
|
-
const embedding = await this.embeddings.generate(currentTask)
|
|
155
|
-
results = await collection.query({
|
|
156
|
-
queryEmbeddings: [embedding],
|
|
157
|
-
nResults: 5,
|
|
158
|
-
where,
|
|
159
|
-
include: ['documents', 'metadatas', 'distances']
|
|
160
|
-
})
|
|
161
|
-
} else {
|
|
162
|
-
results = await collection.query({
|
|
163
|
-
queryTexts: [currentTask],
|
|
164
|
-
nResults: 5,
|
|
165
|
-
where,
|
|
166
|
-
include: ['documents', 'metadatas', 'distances']
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (!results.ids || !results.ids[0]) return []
|
|
171
|
-
|
|
172
|
-
const topics: AnticipatedContext[] = []
|
|
173
|
-
const documents = results.documents?.[0] || []
|
|
174
|
-
const metadatas = results.metadatas?.[0] || []
|
|
175
|
-
const distances = results.distances?.[0] || []
|
|
176
|
-
|
|
177
|
-
for (let i = 0; i < documents.length; i++) {
|
|
178
|
-
const similarity = 1 - (distances[i] || 0)
|
|
179
|
-
if (similarity < 0.3) continue
|
|
180
|
-
|
|
181
|
-
const content = (documents[i] || '').slice(0, 100)
|
|
182
|
-
const patternType = (metadatas[i] as any)?.pattern_type || 'unknown'
|
|
183
|
-
|
|
184
|
-
topics.push({
|
|
185
|
-
topic: `${patternType}: ${content}`,
|
|
186
|
-
relevance: similarity,
|
|
187
|
-
relatedDecisions: 1,
|
|
188
|
-
suggestedQueries: [`Show ${patternType} patterns related to this`]
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return topics
|
|
193
|
-
} catch (error) {
|
|
194
|
-
this.logger.debug({ error }, 'Failed to find related topics')
|
|
195
|
-
return []
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|