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,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Graph Schema
|
|
3
|
+
* Type definitions for the knowledge graph data structures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type EntityType =
|
|
7
|
+
| 'project'
|
|
8
|
+
| 'technology'
|
|
9
|
+
| 'concept'
|
|
10
|
+
| 'person'
|
|
11
|
+
| 'file'
|
|
12
|
+
| 'decision'
|
|
13
|
+
| 'pattern'
|
|
14
|
+
| 'correction'
|
|
15
|
+
| 'date'
|
|
16
|
+
|
|
17
|
+
export type RelationshipType =
|
|
18
|
+
| 'uses'
|
|
19
|
+
| 'depends_on'
|
|
20
|
+
| 'replaces'
|
|
21
|
+
| 'contradicts'
|
|
22
|
+
| 'relates_to'
|
|
23
|
+
| 'part_of'
|
|
24
|
+
| 'created_by'
|
|
25
|
+
| 'decided_in'
|
|
26
|
+
| 'similar_to'
|
|
27
|
+
| 'precedes'
|
|
28
|
+
| 'follows'
|
|
29
|
+
|
|
30
|
+
export interface GraphNode {
|
|
31
|
+
id: string
|
|
32
|
+
name: string
|
|
33
|
+
type: EntityType
|
|
34
|
+
properties: Record<string, unknown>
|
|
35
|
+
created_at: string
|
|
36
|
+
updated_at: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface GraphEdge {
|
|
40
|
+
id: string
|
|
41
|
+
source: string
|
|
42
|
+
target: string
|
|
43
|
+
relationship: RelationshipType
|
|
44
|
+
weight: number
|
|
45
|
+
properties: Record<string, unknown>
|
|
46
|
+
created_at: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TraversalOptions {
|
|
50
|
+
maxDepth?: number
|
|
51
|
+
nodeTypes?: EntityType[]
|
|
52
|
+
relationshipTypes?: RelationshipType[]
|
|
53
|
+
limit?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface GraphSearchOptions {
|
|
57
|
+
query?: string
|
|
58
|
+
nodeType?: EntityType
|
|
59
|
+
relationshipType?: RelationshipType
|
|
60
|
+
startNodeId?: string
|
|
61
|
+
hops?: number
|
|
62
|
+
limit?: number
|
|
63
|
+
alpha?: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface GraphSearchResult {
|
|
67
|
+
nodes: GraphNode[]
|
|
68
|
+
edges: GraphEdge[]
|
|
69
|
+
paths: string[][]
|
|
70
|
+
scores: Map<string, number>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface KnowledgeGraph {
|
|
74
|
+
addNode(node: Omit<GraphNode, 'id' | 'created_at' | 'updated_at'>): GraphNode
|
|
75
|
+
getNode(id: string): GraphNode | undefined
|
|
76
|
+
updateNode(id: string, updates: Partial<Omit<GraphNode, 'id' | 'created_at'>>): GraphNode | undefined
|
|
77
|
+
deleteNode(id: string): boolean
|
|
78
|
+
|
|
79
|
+
addEdge(edge: Omit<GraphEdge, 'id' | 'created_at'>): GraphEdge
|
|
80
|
+
getEdges(nodeId: string, direction?: 'outgoing' | 'incoming' | 'both'): GraphEdge[]
|
|
81
|
+
deleteEdge(id: string): boolean
|
|
82
|
+
|
|
83
|
+
traverse(startId: string, options?: TraversalOptions): GraphNode[]
|
|
84
|
+
shortestPath(sourceId: string, targetId: string): string[] | null
|
|
85
|
+
findNodes(query: { type?: EntityType; name?: string; properties?: Record<string, unknown> }): GraphNode[]
|
|
86
|
+
|
|
87
|
+
getNodeCount(): number
|
|
88
|
+
getEdgeCount(): number
|
|
89
|
+
|
|
90
|
+
save(path: string): Promise<void>
|
|
91
|
+
load(path: string): Promise<void>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SerializedGraph {
|
|
95
|
+
nodes: GraphNode[]
|
|
96
|
+
edges: GraphEdge[]
|
|
97
|
+
version: string
|
|
98
|
+
saved_at: string
|
|
99
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Search Engine
|
|
3
|
+
* Combines vector similarity with graph proximity for enhanced search
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Logger } from 'pino'
|
|
7
|
+
import type { InMemoryKnowledgeGraph } from './memory-graph'
|
|
8
|
+
import type { GraphNode, GraphEdge, GraphSearchOptions, GraphSearchResult } from './schema'
|
|
9
|
+
|
|
10
|
+
export class GraphSearchEngine {
|
|
11
|
+
private graph: InMemoryKnowledgeGraph
|
|
12
|
+
private logger: Logger
|
|
13
|
+
|
|
14
|
+
constructor(graph: InMemoryKnowledgeGraph, logger: Logger) {
|
|
15
|
+
this.graph = graph
|
|
16
|
+
this.logger = logger.child({ component: 'graph-search' })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
search(options: GraphSearchOptions): GraphSearchResult {
|
|
20
|
+
const {
|
|
21
|
+
query,
|
|
22
|
+
nodeType,
|
|
23
|
+
startNodeId,
|
|
24
|
+
hops = 2,
|
|
25
|
+
limit = 20,
|
|
26
|
+
alpha = 0.6
|
|
27
|
+
} = options
|
|
28
|
+
|
|
29
|
+
const scores = new Map<string, number>()
|
|
30
|
+
const resultNodes: GraphNode[] = []
|
|
31
|
+
const resultEdges: GraphEdge[] = []
|
|
32
|
+
const paths: string[][] = []
|
|
33
|
+
|
|
34
|
+
if (startNodeId) {
|
|
35
|
+
// Graph expansion from a starting node
|
|
36
|
+
return this.expandFromNode(startNodeId, hops, limit, nodeType)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (query) {
|
|
40
|
+
// Text-based search: find nodes matching query, then expand
|
|
41
|
+
const matchingNodes = this.findMatchingNodes(query, nodeType)
|
|
42
|
+
|
|
43
|
+
for (const node of matchingNodes) {
|
|
44
|
+
const nameScore = this.calculateNameScore(node.name, query)
|
|
45
|
+
scores.set(node.id, nameScore)
|
|
46
|
+
|
|
47
|
+
// Expand from each matching node
|
|
48
|
+
const neighbors = this.graph.traverse(node.id, {
|
|
49
|
+
maxDepth: hops,
|
|
50
|
+
nodeTypes: nodeType ? [nodeType] : undefined,
|
|
51
|
+
limit: Math.ceil(limit / Math.max(matchingNodes.length, 1))
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
for (const neighbor of neighbors) {
|
|
55
|
+
if (!scores.has(neighbor.id)) {
|
|
56
|
+
// Graph proximity score: decreases with distance
|
|
57
|
+
const path = this.graph.shortestPath(node.id, neighbor.id)
|
|
58
|
+
const distance = path ? path.length - 1 : hops + 1
|
|
59
|
+
const graphScore = 1 / (1 + distance)
|
|
60
|
+
|
|
61
|
+
// Combined score
|
|
62
|
+
const combinedScore = alpha * nameScore + (1 - alpha) * graphScore
|
|
63
|
+
scores.set(neighbor.id, Math.max(scores.get(neighbor.id) || 0, combinedScore))
|
|
64
|
+
|
|
65
|
+
if (path && path.length > 1) {
|
|
66
|
+
paths.push(path)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Collect nodes sorted by score
|
|
73
|
+
const sortedEntries = Array.from(scores.entries())
|
|
74
|
+
.sort(([, a], [, b]) => b - a)
|
|
75
|
+
.slice(0, limit)
|
|
76
|
+
|
|
77
|
+
for (const [nodeId] of sortedEntries) {
|
|
78
|
+
const node = this.graph.getNode(nodeId)
|
|
79
|
+
if (node) resultNodes.push(node)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Collect edges between result nodes
|
|
83
|
+
const resultNodeIds = new Set(resultNodes.map(n => n.id))
|
|
84
|
+
for (const node of resultNodes) {
|
|
85
|
+
const edges = this.graph.getEdges(node.id)
|
|
86
|
+
for (const edge of edges) {
|
|
87
|
+
if (resultNodeIds.has(edge.source) && resultNodeIds.has(edge.target)) {
|
|
88
|
+
if (!resultEdges.some(e => e.id === edge.id)) {
|
|
89
|
+
resultEdges.push(edge)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { nodes: resultNodes, edges: resultEdges, paths, scores }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
findRelated(nodeId: string, hops: number = 2): GraphSearchResult {
|
|
100
|
+
return this.expandFromNode(nodeId, hops, 50)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private expandFromNode(
|
|
104
|
+
nodeId: string,
|
|
105
|
+
hops: number,
|
|
106
|
+
limit: number,
|
|
107
|
+
nodeType?: string
|
|
108
|
+
): GraphSearchResult {
|
|
109
|
+
const scores = new Map<string, number>()
|
|
110
|
+
const nodes = this.graph.traverse(nodeId, {
|
|
111
|
+
maxDepth: hops,
|
|
112
|
+
nodeTypes: nodeType ? [nodeType as any] : undefined,
|
|
113
|
+
limit
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const resultEdges: GraphEdge[] = []
|
|
117
|
+
const paths: string[][] = []
|
|
118
|
+
const nodeIds = new Set(nodes.map(n => n.id))
|
|
119
|
+
|
|
120
|
+
for (const node of nodes) {
|
|
121
|
+
const path = this.graph.shortestPath(nodeId, node.id)
|
|
122
|
+
const distance = path ? path.length - 1 : 0
|
|
123
|
+
scores.set(node.id, 1 / (1 + distance))
|
|
124
|
+
|
|
125
|
+
if (path && path.length > 1) {
|
|
126
|
+
paths.push(path)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const edges = this.graph.getEdges(node.id)
|
|
130
|
+
for (const edge of edges) {
|
|
131
|
+
if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
|
|
132
|
+
if (!resultEdges.some(e => e.id === edge.id)) {
|
|
133
|
+
resultEdges.push(edge)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { nodes, edges: resultEdges, paths, scores }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private findMatchingNodes(query: string, nodeType?: string): GraphNode[] {
|
|
143
|
+
const lowerQuery = query.toLowerCase()
|
|
144
|
+
const queryTerms = lowerQuery.split(/\s+/).filter(t => t.length > 1)
|
|
145
|
+
|
|
146
|
+
return this.graph.findNodes({
|
|
147
|
+
type: nodeType as any,
|
|
148
|
+
name: undefined
|
|
149
|
+
}).filter(node => {
|
|
150
|
+
const lowerName = node.name.toLowerCase()
|
|
151
|
+
return queryTerms.some(term => lowerName.includes(term) || term.includes(lowerName))
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private calculateNameScore(nodeName: string, query: string): number {
|
|
156
|
+
const lowerName = nodeName.toLowerCase()
|
|
157
|
+
const lowerQuery = query.toLowerCase()
|
|
158
|
+
|
|
159
|
+
if (lowerName === lowerQuery) return 1.0
|
|
160
|
+
if (lowerQuery.includes(lowerName)) return 0.9
|
|
161
|
+
if (lowerName.includes(lowerQuery)) return 0.8
|
|
162
|
+
|
|
163
|
+
// Partial term matching
|
|
164
|
+
const queryTerms = lowerQuery.split(/\s+/)
|
|
165
|
+
const matchCount = queryTerms.filter(t => lowerName.includes(t)).length
|
|
166
|
+
return matchCount > 0 ? 0.5 + (0.3 * matchCount / queryTerms.length) : 0.3
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relationship Extractor
|
|
3
|
+
* Pattern-based extraction of relationships between entities
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RelationshipType } from './graph/schema'
|
|
7
|
+
import type { ExtractedEntity } from './entity-extractor'
|
|
8
|
+
|
|
9
|
+
export interface ExtractedRelationship {
|
|
10
|
+
sourceEntity: string
|
|
11
|
+
targetEntity: string
|
|
12
|
+
relationship: RelationshipType
|
|
13
|
+
confidence: number
|
|
14
|
+
evidence: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RelationshipPattern {
|
|
18
|
+
regex: RegExp
|
|
19
|
+
relationship: RelationshipType
|
|
20
|
+
confidence: number
|
|
21
|
+
sourceGroup: number
|
|
22
|
+
targetGroup: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PATTERNS: RelationshipPattern[] = [
|
|
26
|
+
// "chose X for Y", "use X for Y", "selected X for Y"
|
|
27
|
+
{ regex: /(?:chose|choose|use|using|selected|picked)\s+(\w[\w.-]*)\s+(?:for|in|with)\s+(\w[\w.-]*)/gi, relationship: 'uses', confidence: 0.85, sourceGroup: 2, targetGroup: 1 },
|
|
28
|
+
// "X uses Y", "X depends on Y"
|
|
29
|
+
{ regex: /(\w[\w.-]*)\s+(?:uses|utilizes|leverages|employs)\s+(\w[\w.-]*)/gi, relationship: 'uses', confidence: 0.8, sourceGroup: 1, targetGroup: 2 },
|
|
30
|
+
// "X depends on Y", "X requires Y"
|
|
31
|
+
{ regex: /(\w[\w.-]*)\s+(?:depends?\s+on|requires|needs)\s+(\w[\w.-]*)/gi, relationship: 'depends_on', confidence: 0.85, sourceGroup: 1, targetGroup: 2 },
|
|
32
|
+
// "replaced X with Y", "migrated from X to Y"
|
|
33
|
+
{ regex: /(?:replaced|replace|swap(?:ped)?)\s+(\w[\w.-]*)\s+(?:with|for)\s+(\w[\w.-]*)/gi, relationship: 'replaces', confidence: 0.9, sourceGroup: 2, targetGroup: 1 },
|
|
34
|
+
{ regex: /(?:migrated?|moved?|switched?)\s+(?:from\s+)?(\w[\w.-]*)\s+to\s+(\w[\w.-]*)/gi, relationship: 'replaces', confidence: 0.85, sourceGroup: 2, targetGroup: 1 },
|
|
35
|
+
// "X contradicts Y", "X conflicts with Y"
|
|
36
|
+
{ regex: /(\w[\w.-]*)\s+(?:contradicts|conflicts?\s+with|incompatible\s+with)\s+(\w[\w.-]*)/gi, relationship: 'contradicts', confidence: 0.8, sourceGroup: 1, targetGroup: 2 },
|
|
37
|
+
// "X is part of Y", "X belongs to Y"
|
|
38
|
+
{ regex: /(\w[\w.-]*)\s+(?:is\s+part\s+of|belongs?\s+to|included?\s+in)\s+(\w[\w.-]*)/gi, relationship: 'part_of', confidence: 0.8, sourceGroup: 1, targetGroup: 2 },
|
|
39
|
+
// "X relates to Y", "X is related to Y"
|
|
40
|
+
{ regex: /(\w[\w.-]*)\s+(?:relates?\s+to|is\s+related\s+to|connects?\s+(?:to|with))\s+(\w[\w.-]*)/gi, relationship: 'relates_to', confidence: 0.7, sourceGroup: 1, targetGroup: 2 },
|
|
41
|
+
// "X is similar to Y", "X like Y"
|
|
42
|
+
{ regex: /(\w[\w.-]*)\s+(?:is\s+similar\s+to|like|resembles|comparable\s+to)\s+(\w[\w.-]*)/gi, relationship: 'similar_to', confidence: 0.7, sourceGroup: 1, targetGroup: 2 },
|
|
43
|
+
// "after X, Y" / "X then Y" (temporal sequencing)
|
|
44
|
+
{ regex: /(?:after|following)\s+(\w[\w.-]*)\s*,?\s*(?:we\s+)?(?:use|chose|moved\s+to|adopted)\s+(\w[\w.-]*)/gi, relationship: 'follows', confidence: 0.7, sourceGroup: 2, targetGroup: 1 },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
export class RelationshipExtractor {
|
|
48
|
+
extract(text: string, entities: ExtractedEntity[]): ExtractedRelationship[] {
|
|
49
|
+
const relationships: ExtractedRelationship[] = []
|
|
50
|
+
const entityNames = new Set(entities.map(e => e.normalizedName))
|
|
51
|
+
|
|
52
|
+
for (const pattern of PATTERNS) {
|
|
53
|
+
let match: RegExpExecArray | null
|
|
54
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags)
|
|
55
|
+
|
|
56
|
+
while ((match = regex.exec(text)) !== null) {
|
|
57
|
+
const rawSource = match[pattern.sourceGroup]?.toLowerCase()
|
|
58
|
+
const rawTarget = match[pattern.targetGroup]?.toLowerCase()
|
|
59
|
+
|
|
60
|
+
if (!rawSource || !rawTarget) continue
|
|
61
|
+
if (rawSource === rawTarget) continue
|
|
62
|
+
|
|
63
|
+
// Check if at least one entity is known
|
|
64
|
+
const sourceKnown = entityNames.has(rawSource) || this.findEntityMatch(rawSource, entities)
|
|
65
|
+
const targetKnown = entityNames.has(rawTarget) || this.findEntityMatch(rawTarget, entities)
|
|
66
|
+
|
|
67
|
+
if (!sourceKnown && !targetKnown) continue
|
|
68
|
+
|
|
69
|
+
const sourceName = this.resolveEntityName(rawSource, entities) || rawSource
|
|
70
|
+
const targetName = this.resolveEntityName(rawTarget, entities) || rawTarget
|
|
71
|
+
|
|
72
|
+
// Avoid duplicates
|
|
73
|
+
const exists = relationships.some(r =>
|
|
74
|
+
r.sourceEntity === sourceName &&
|
|
75
|
+
r.targetEntity === targetName &&
|
|
76
|
+
r.relationship === pattern.relationship
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (!exists) {
|
|
80
|
+
relationships.push({
|
|
81
|
+
sourceEntity: sourceName,
|
|
82
|
+
targetEntity: targetName,
|
|
83
|
+
relationship: pattern.relationship,
|
|
84
|
+
confidence: pattern.confidence * (sourceKnown && targetKnown ? 1.0 : 0.8),
|
|
85
|
+
evidence: match[0].trim()
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return relationships
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private findEntityMatch(name: string, entities: ExtractedEntity[]): boolean {
|
|
95
|
+
return entities.some(e =>
|
|
96
|
+
e.normalizedName === name ||
|
|
97
|
+
e.name.toLowerCase() === name
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private resolveEntityName(name: string, entities: ExtractedEntity[]): string | undefined {
|
|
102
|
+
const entity = entities.find(e =>
|
|
103
|
+
e.normalizedName === name ||
|
|
104
|
+
e.name.toLowerCase() === name
|
|
105
|
+
)
|
|
106
|
+
return entity?.normalizedName
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ChromaClient, Collection } from 'chromadb'
|
|
2
|
+
import type { Logger } from 'pino'
|
|
3
|
+
import type { ChromaConfig } from './config'
|
|
4
|
+
|
|
5
|
+
export class ChromaClientManager {
|
|
6
|
+
private client: ChromaClient | null = null
|
|
7
|
+
private logger: Logger
|
|
8
|
+
private config: ChromaConfig
|
|
9
|
+
private isConnected: boolean = false
|
|
10
|
+
private collections: Map<string, Collection> = new Map()
|
|
11
|
+
|
|
12
|
+
constructor(logger: Logger, config: ChromaConfig) {
|
|
13
|
+
this.logger = logger.child({ component: 'chroma-client' })
|
|
14
|
+
this.config = config
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async initialize(): Promise<void> {
|
|
18
|
+
this.logger.info({ mode: this.config.mode }, 'Initializing ChromaDB client')
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const authConfig = this.config.chromaAuthToken ? {
|
|
22
|
+
headers: {
|
|
23
|
+
'Authorization': `Bearer ${this.config.chromaAuthToken}`,
|
|
24
|
+
'X-Chroma-Token': this.config.chromaAuthToken
|
|
25
|
+
},
|
|
26
|
+
...(this.config.chromaTenant ? { tenant: this.config.chromaTenant } : {}),
|
|
27
|
+
...(this.config.chromaDatabase ? { database: this.config.chromaDatabase } : {})
|
|
28
|
+
} : {}
|
|
29
|
+
|
|
30
|
+
switch (this.config.mode) {
|
|
31
|
+
case 'persistent':
|
|
32
|
+
this.client = new ChromaClient({
|
|
33
|
+
path: this.config.path,
|
|
34
|
+
...authConfig
|
|
35
|
+
})
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
case 'client-server':
|
|
39
|
+
if (!this.config.host || !this.config.port) {
|
|
40
|
+
throw new Error('Host and port required for client-server mode')
|
|
41
|
+
}
|
|
42
|
+
this.client = new ChromaClient({
|
|
43
|
+
host: this.config.host,
|
|
44
|
+
port: this.config.port,
|
|
45
|
+
...authConfig
|
|
46
|
+
})
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
case 'cloud':
|
|
50
|
+
if (!this.config.host) {
|
|
51
|
+
throw new Error('Host URL required for cloud mode')
|
|
52
|
+
}
|
|
53
|
+
const hostWithProtocol = this.config.host.startsWith('http')
|
|
54
|
+
? this.config.host
|
|
55
|
+
: `https://${this.config.host}`
|
|
56
|
+
const url = new URL(hostWithProtocol)
|
|
57
|
+
this.client = new ChromaClient({
|
|
58
|
+
host: url.hostname,
|
|
59
|
+
port: url.port ? parseInt(url.port) : (url.protocol === 'https:' ? 443 : 80),
|
|
60
|
+
ssl: url.protocol === 'https:',
|
|
61
|
+
...authConfig
|
|
62
|
+
})
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
case 'ephemeral':
|
|
66
|
+
try {
|
|
67
|
+
this.client = new ChromaClient()
|
|
68
|
+
} catch (err) {
|
|
69
|
+
this.logger.warn({ error: err }, 'Ephemeral mode failed, attempting client-server mode')
|
|
70
|
+
this.client = new ChromaClient({
|
|
71
|
+
host: 'localhost',
|
|
72
|
+
port: 8000,
|
|
73
|
+
ssl: false,
|
|
74
|
+
...authConfig
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await this.client.heartbeat()
|
|
82
|
+
this.isConnected = true
|
|
83
|
+
this.logger.info('ChromaDB client initialized successfully')
|
|
84
|
+
} catch (heartbeatError) {
|
|
85
|
+
this.logger.warn({ error: heartbeatError }, 'ChromaDB heartbeat failed, connection may not be fully established')
|
|
86
|
+
this.isConnected = true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.logger.error({ error }, 'Failed to initialize ChromaDB client')
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async getCollection(name: string): Promise<Collection> {
|
|
96
|
+
if (!this.client) {
|
|
97
|
+
throw new Error('ChromaDB client not initialized')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (this.collections.has(name)) {
|
|
101
|
+
return this.collections.get(name)!
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const collection = await this.client.getOrCreateCollection({
|
|
106
|
+
name,
|
|
107
|
+
metadata: {
|
|
108
|
+
'hnsw:space': 'cosine',
|
|
109
|
+
'created_at': new Date().toISOString()
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
this.collections.set(name, collection)
|
|
114
|
+
this.logger.info({ collection: name }, 'Collection retrieved/created')
|
|
115
|
+
|
|
116
|
+
return collection
|
|
117
|
+
|
|
118
|
+
} catch (error) {
|
|
119
|
+
this.logger.error({ error, collection: name }, 'Failed to get collection')
|
|
120
|
+
throw error
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async listCollections(): Promise<string[]> {
|
|
125
|
+
if (!this.client) {
|
|
126
|
+
throw new Error('ChromaDB client not initialized')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const collections = await this.client.listCollections()
|
|
130
|
+
return collections.map(c => c.name)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async deleteCollection(name: string): Promise<void> {
|
|
134
|
+
if (!this.client) {
|
|
135
|
+
throw new Error('ChromaDB client not initialized')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await this.client.deleteCollection({ name })
|
|
139
|
+
this.collections.delete(name)
|
|
140
|
+
this.logger.info({ collection: name }, 'Collection deleted')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async healthCheck(): Promise<boolean> {
|
|
144
|
+
if (!this.client) return false
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await this.client.heartbeat()
|
|
148
|
+
return true
|
|
149
|
+
} catch {
|
|
150
|
+
this.isConnected = false
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getStatus(): { connected: boolean; mode: string; collections: number } {
|
|
156
|
+
return {
|
|
157
|
+
connected: this.isConnected,
|
|
158
|
+
mode: this.config.mode,
|
|
159
|
+
collections: this.collections.size
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async close(): Promise<void> {
|
|
164
|
+
this.collections.clear()
|
|
165
|
+
this.client = null
|
|
166
|
+
this.isConnected = false
|
|
167
|
+
this.logger.info('ChromaDB client closed')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Collection } from 'chromadb'
|
|
2
|
+
import type { Logger } from 'pino'
|
|
3
|
+
import type { ChromaClientManager } from './client'
|
|
4
|
+
import { COLLECTIONS, type CollectionName } from './schemas'
|
|
5
|
+
|
|
6
|
+
export class CollectionManager {
|
|
7
|
+
private logger: Logger
|
|
8
|
+
private client: ChromaClientManager
|
|
9
|
+
private initialized: boolean = false
|
|
10
|
+
|
|
11
|
+
constructor(logger: Logger, client: ChromaClientManager) {
|
|
12
|
+
this.logger = logger.child({ component: 'collection-manager' })
|
|
13
|
+
this.client = client
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async initialize(): Promise<void> {
|
|
17
|
+
this.logger.info('Initializing collections')
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
for (const collectionName of Object.values(COLLECTIONS)) {
|
|
21
|
+
await this.client.getCollection(collectionName)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.initialized = true
|
|
25
|
+
this.logger.info('All collections initialized')
|
|
26
|
+
|
|
27
|
+
} catch (error) {
|
|
28
|
+
this.logger.error({ error }, 'Failed to initialize collections')
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getDecisions(): Promise<Collection> {
|
|
34
|
+
return this.client.getCollection(COLLECTIONS.DECISIONS)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getMemories(): Promise<Collection> {
|
|
38
|
+
return this.client.getCollection(COLLECTIONS.MEMORIES)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getPatterns(): Promise<Collection> {
|
|
42
|
+
return this.client.getCollection(COLLECTIONS.PATTERNS)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getCorrections(): Promise<Collection> {
|
|
46
|
+
return this.client.getCollection(COLLECTIONS.CORRECTIONS)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getFeedback(): Promise<Collection> {
|
|
50
|
+
return this.client.getCollection(COLLECTIONS.FEEDBACK)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getEpisodes(): Promise<Collection> {
|
|
54
|
+
return this.client.getCollection(COLLECTIONS.EPISODES)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getArchivedMemories(): Promise<Collection> {
|
|
58
|
+
return this.client.getCollection(COLLECTIONS.ARCHIVED_MEMORIES)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getProjectCollection(projectName: string): Promise<Collection> {
|
|
62
|
+
const collectionName = `project_${projectName.toLowerCase().replace(/[^a-z0-9]/g, '_')}`
|
|
63
|
+
return this.client.getCollection(collectionName)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getStats(): Promise<Record<string, number>> {
|
|
67
|
+
const stats: Record<string, number> = {}
|
|
68
|
+
|
|
69
|
+
for (const collectionName of Object.values(COLLECTIONS)) {
|
|
70
|
+
try {
|
|
71
|
+
const collection = await this.client.getCollection(collectionName)
|
|
72
|
+
stats[collectionName] = await collection.count()
|
|
73
|
+
} catch {
|
|
74
|
+
stats[collectionName] = 0
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return stats
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async clearAll(): Promise<void> {
|
|
82
|
+
this.logger.warn('Clearing all collections')
|
|
83
|
+
|
|
84
|
+
for (const collectionName of Object.values(COLLECTIONS)) {
|
|
85
|
+
try {
|
|
86
|
+
await this.client.deleteCollection(collectionName)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.logger.error({ error, collection: collectionName }, 'Failed to delete collection')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await this.initialize()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const ChromaConfigSchema = z.object({
|
|
4
|
+
mode: z.enum(['persistent', 'client-server', 'ephemeral', 'cloud']).default('persistent'),
|
|
5
|
+
path: z.string().default('./data/chroma'),
|
|
6
|
+
host: z.string().optional(),
|
|
7
|
+
port: z.number().optional(),
|
|
8
|
+
embeddingProvider: z.enum(['default', 'transformers', 'openai']).default('default'),
|
|
9
|
+
embeddingModel: z.string().default('Xenova/all-MiniLM-L6-v2'),
|
|
10
|
+
defaultCollection: z.string().default('memories'),
|
|
11
|
+
batchSize: z.number().default(100),
|
|
12
|
+
openaiApiKey: z.string().optional(),
|
|
13
|
+
chromaAuthToken: z.string().optional(),
|
|
14
|
+
chromaTenant: z.string().optional(),
|
|
15
|
+
chromaDatabase: z.string().optional()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export type ChromaConfig = z.infer<typeof ChromaConfigSchema>
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_CHROMA_CONFIG: ChromaConfig = {
|
|
21
|
+
mode: 'persistent',
|
|
22
|
+
path: './data/chroma',
|
|
23
|
+
embeddingProvider: 'default',
|
|
24
|
+
embeddingModel: 'Xenova/all-MiniLM-L6-v2',
|
|
25
|
+
defaultCollection: 'memories',
|
|
26
|
+
batchSize: 100
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getChromaConfigFromEnv(): Partial<ChromaConfig> {
|
|
30
|
+
const config: Partial<ChromaConfig> = {
|
|
31
|
+
chromaAuthToken: process.env.CHROMA_API_KEY,
|
|
32
|
+
chromaTenant: process.env.CHROMA_TENANT,
|
|
33
|
+
chromaDatabase: process.env.CHROMA_DATABASE
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (process.env.CHROMA_HOST) {
|
|
37
|
+
config.host = process.env.CHROMA_HOST
|
|
38
|
+
config.mode = 'cloud'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.env.CHROMA_EMBEDDING_PROVIDER) {
|
|
42
|
+
config.embeddingProvider = process.env.CHROMA_EMBEDDING_PROVIDER as any
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return config
|
|
46
|
+
}
|