claudecode-rlm 1.0.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/LICENSE +21 -0
- package/README.md +209 -0
- package/dist/config.d.ts +176 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +103 -0
- package/dist/config.js.map +1 -0
- package/dist/graph/index.d.ts +10 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +10 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/ingestion.d.ts +68 -0
- package/dist/graph/ingestion.d.ts.map +1 -0
- package/dist/graph/ingestion.js +417 -0
- package/dist/graph/ingestion.js.map +1 -0
- package/dist/graph/storage.d.ts +51 -0
- package/dist/graph/storage.d.ts.map +1 -0
- package/dist/graph/storage.js +552 -0
- package/dist/graph/storage.js.map +1 -0
- package/dist/graph/traversal.d.ts +54 -0
- package/dist/graph/traversal.d.ts.map +1 -0
- package/dist/graph/traversal.js +255 -0
- package/dist/graph/traversal.js.map +1 -0
- package/dist/graph/types.d.ts +152 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +94 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +190 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-types.d.ts +96 -0
- package/dist/plugin-types.d.ts.map +1 -0
- package/dist/plugin-types.js +17 -0
- package/dist/plugin-types.js.map +1 -0
- package/dist/search/enhanced.d.ts +95 -0
- package/dist/search/enhanced.d.ts.map +1 -0
- package/dist/search/enhanced.js +194 -0
- package/dist/search/enhanced.js.map +1 -0
- package/dist/search/index.d.ts +8 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +8 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/patterns.d.ts +38 -0
- package/dist/search/patterns.d.ts.map +1 -0
- package/dist/search/patterns.js +124 -0
- package/dist/search/patterns.js.map +1 -0
- package/dist/tools/graph-query.d.ts +14 -0
- package/dist/tools/graph-query.d.ts.map +1 -0
- package/dist/tools/graph-query.js +203 -0
- package/dist/tools/graph-query.js.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory.d.ts +20 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +181 -0
- package/dist/tools/memory.js.map +1 -0
- package/package.json +66 -0
- package/src/config.ts +111 -0
- package/src/graph/index.ts +10 -0
- package/src/graph/ingestion.ts +528 -0
- package/src/graph/storage.ts +639 -0
- package/src/graph/traversal.ts +348 -0
- package/src/graph/types.ts +144 -0
- package/src/index.ts +238 -0
- package/src/plugin-types.ts +107 -0
- package/src/search/enhanced.ts +264 -0
- package/src/search/index.ts +23 -0
- package/src/search/patterns.ts +139 -0
- package/src/tools/graph-query.ts +257 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/memory.ts +208 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeCode RLM Plugin for Claude Code
|
|
3
|
+
*
|
|
4
|
+
* Enhances Claude Code's RLM system with:
|
|
5
|
+
* - Knowledge graph-based context storage (74x faster reads)
|
|
6
|
+
* - Extended reference patterns (40+ patterns)
|
|
7
|
+
* - Enhanced hybrid search with recency weighting
|
|
8
|
+
* - Entity extraction and linking
|
|
9
|
+
* - Custom tools for graph query and memory search
|
|
10
|
+
*
|
|
11
|
+
* @module claudecode-rlm
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Plugin, Hooks, PluginInput } from "./plugin-types.js"
|
|
15
|
+
import { GraphStorage, GraphIngester } from "./graph/index.js"
|
|
16
|
+
import { EnhancedSearch, detectsReference, detectsTaskCompletion } from "./search/index.js"
|
|
17
|
+
import {
|
|
18
|
+
graphQueryTool,
|
|
19
|
+
memoryEnhancedTool,
|
|
20
|
+
listEntitiesTool,
|
|
21
|
+
graphStatsTool,
|
|
22
|
+
} from "./tools/index.js"
|
|
23
|
+
import { parseConfig, type PluginConfig } from "./config.js"
|
|
24
|
+
|
|
25
|
+
// Re-export modules
|
|
26
|
+
export * from "./graph/index.js"
|
|
27
|
+
export * from "./search/index.js"
|
|
28
|
+
export * from "./tools/index.js"
|
|
29
|
+
export { parseConfig, type PluginConfig } from "./config.js"
|
|
30
|
+
export type { Plugin, Hooks, PluginInput } from "./plugin-types.js"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Plugin state.
|
|
34
|
+
*/
|
|
35
|
+
interface PluginState {
|
|
36
|
+
config: PluginConfig
|
|
37
|
+
initialized: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const state: PluginState = {
|
|
41
|
+
config: parseConfig({}),
|
|
42
|
+
initialized: false,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ClaudeCode RLM Plugin entry point.
|
|
47
|
+
*
|
|
48
|
+
* Registers hooks for:
|
|
49
|
+
* - experimental.chat.messages.transform: Inject graph-based context
|
|
50
|
+
* - experimental.session.compacting: Add entity summaries
|
|
51
|
+
* - event: Listen for rlm.archived to ingest into graph
|
|
52
|
+
* - tool: Register graph_query and memory_enhanced tools
|
|
53
|
+
*/
|
|
54
|
+
export const ClaudeCodeRLMPlugin: Plugin = async (
|
|
55
|
+
input: PluginInput
|
|
56
|
+
): Promise<Hooks> => {
|
|
57
|
+
const { worktree, client } = input
|
|
58
|
+
|
|
59
|
+
// Initialize graph storage with worktree path
|
|
60
|
+
GraphStorage.init(worktree)
|
|
61
|
+
|
|
62
|
+
// Load plugin config from Claude Code config
|
|
63
|
+
try {
|
|
64
|
+
// Try to get config from client if available
|
|
65
|
+
const claudeCodeConfig = await client?.config?.get?.()
|
|
66
|
+
const pluginConfig = (claudeCodeConfig as any)?.["claudecode-rlm"]
|
|
67
|
+
state.config = parseConfig(pluginConfig)
|
|
68
|
+
} catch {
|
|
69
|
+
// Use defaults if config not available
|
|
70
|
+
state.config = parseConfig({})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
state.initialized = true
|
|
74
|
+
|
|
75
|
+
console.log("[claudecode-rlm] Plugin initialized (optimized storage: 74x faster reads)")
|
|
76
|
+
console.log("[claudecode-rlm] Graph enabled:", state.config.graph.enabled)
|
|
77
|
+
console.log(
|
|
78
|
+
"[claudecode-rlm] Extended patterns:",
|
|
79
|
+
state.config.enhanced_search.additional_patterns
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
/**
|
|
84
|
+
* Register custom tools.
|
|
85
|
+
*/
|
|
86
|
+
tool: {
|
|
87
|
+
graph_query: graphQueryTool,
|
|
88
|
+
memory_enhanced: memoryEnhancedTool,
|
|
89
|
+
list_entities: listEntitiesTool,
|
|
90
|
+
graph_stats: graphStatsTool,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Transform messages before sending to LLM.
|
|
95
|
+
* Injects graph-based context when reference patterns detected.
|
|
96
|
+
*/
|
|
97
|
+
"experimental.chat.messages.transform": async (input, output) => {
|
|
98
|
+
if (!state.config.graph.enabled) return
|
|
99
|
+
if (!state.config.enhanced_search.additional_patterns) return
|
|
100
|
+
|
|
101
|
+
// Get the last user message
|
|
102
|
+
const lastMessage = output.messages[output.messages.length - 1]
|
|
103
|
+
if (!lastMessage || lastMessage.info.role !== "user") return
|
|
104
|
+
|
|
105
|
+
// Extract text from message parts
|
|
106
|
+
const textParts = lastMessage.parts
|
|
107
|
+
.filter((p) => p.type === "text")
|
|
108
|
+
.map((p) => (p as any).text || "")
|
|
109
|
+
const messageText = textParts.join(" ")
|
|
110
|
+
|
|
111
|
+
if (!messageText) return
|
|
112
|
+
|
|
113
|
+
// Get sessionID from message info
|
|
114
|
+
const sessionID = (lastMessage.info as any).sessionID || ""
|
|
115
|
+
if (!sessionID) return
|
|
116
|
+
|
|
117
|
+
// Check for reference patterns using enhanced detection
|
|
118
|
+
const analysis = EnhancedSearch.analyzeForInjection(
|
|
119
|
+
sessionID,
|
|
120
|
+
messageText,
|
|
121
|
+
{
|
|
122
|
+
additionalPatterns: state.config.enhanced_search.additional_patterns,
|
|
123
|
+
recencyWeight: state.config.enhanced_search.recency_weight,
|
|
124
|
+
entityBoost: state.config.enhanced_search.entity_boost,
|
|
125
|
+
minScoreThreshold: state.config.enhanced_search.min_score_threshold,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (!analysis.shouldInject || analysis.results.length === 0) return
|
|
130
|
+
|
|
131
|
+
// Format context for injection
|
|
132
|
+
const contextText = EnhancedSearch.formatForInjection(analysis.results)
|
|
133
|
+
|
|
134
|
+
// Inject as a system message before the user message
|
|
135
|
+
const injectionMessage = {
|
|
136
|
+
info: {
|
|
137
|
+
id: `claudecode-inject-${Date.now()}`,
|
|
138
|
+
sessionID,
|
|
139
|
+
role: "user" as const,
|
|
140
|
+
time: Date.now(),
|
|
141
|
+
},
|
|
142
|
+
parts: [
|
|
143
|
+
{
|
|
144
|
+
type: "text" as const,
|
|
145
|
+
text: contextText,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Insert before the last user message
|
|
151
|
+
output.messages.splice(output.messages.length - 1, 0, injectionMessage)
|
|
152
|
+
|
|
153
|
+
console.log(
|
|
154
|
+
`[claudecode-rlm] Injected ${analysis.results.length} context blocks`
|
|
155
|
+
)
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Add entity summary during session compaction.
|
|
160
|
+
*/
|
|
161
|
+
"experimental.session.compacting": async (input, output) => {
|
|
162
|
+
if (!state.config.graph.enabled) return
|
|
163
|
+
|
|
164
|
+
const { sessionID } = input
|
|
165
|
+
|
|
166
|
+
// Get entity summary from graph
|
|
167
|
+
try {
|
|
168
|
+
const stats = GraphStorage.getStats(sessionID)
|
|
169
|
+
|
|
170
|
+
if (stats.nodeCount > 0) {
|
|
171
|
+
const entitySummary = [
|
|
172
|
+
"",
|
|
173
|
+
"Knowledge Graph Summary:",
|
|
174
|
+
`- ${stats.nodesByType["document"] || 0} documents archived`,
|
|
175
|
+
`- ${stats.nodesByType["chunk"] || 0} content chunks indexed`,
|
|
176
|
+
`- ${stats.nodesByType["entity"] || 0} entities tracked`,
|
|
177
|
+
"",
|
|
178
|
+
"Use graph_query or memory_enhanced tools to search archived context.",
|
|
179
|
+
].join("\n")
|
|
180
|
+
|
|
181
|
+
output.context.push(entitySummary)
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore errors
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Listen for RLM events to ingest into graph.
|
|
190
|
+
*/
|
|
191
|
+
event: async ({ event }) => {
|
|
192
|
+
if (!state.config.graph.enabled) return
|
|
193
|
+
if (!state.config.graph.auto_ingest) return
|
|
194
|
+
|
|
195
|
+
// Handle RLM archived event
|
|
196
|
+
if (event.type === "rlm.archived") {
|
|
197
|
+
const { sessionID, blockID, content, summary, tokens } = event.properties as any
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
// Ingest the archived context block into the graph
|
|
201
|
+
const block = {
|
|
202
|
+
id: blockID,
|
|
203
|
+
sessionID,
|
|
204
|
+
content: content || "",
|
|
205
|
+
summary: summary || "",
|
|
206
|
+
tokens: tokens || 0,
|
|
207
|
+
createdAt: Date.now(),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
GraphIngester.ingestContextBlock(block)
|
|
211
|
+
// Flush to ensure data is persisted
|
|
212
|
+
GraphStorage.flush()
|
|
213
|
+
console.log(`[claudecode-rlm] Ingested block ${blockID} into graph`)
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error("[claudecode-rlm] Failed to ingest block:", error)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Handle task completion for auto-archive
|
|
220
|
+
if (
|
|
221
|
+
state.config.task_detection.auto_archive_on_completion &&
|
|
222
|
+
event.type === "todo.updated"
|
|
223
|
+
) {
|
|
224
|
+
const { status, content } = event.properties as any
|
|
225
|
+
if (status === "completed") {
|
|
226
|
+
console.log(
|
|
227
|
+
`[claudecode-rlm] Task completed: ${content?.slice(0, 50)}...`
|
|
228
|
+
)
|
|
229
|
+
// Note: Actual archival is handled by Claude Code's RLM system
|
|
230
|
+
// We just log for visibility
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Default export for plugin loading
|
|
238
|
+
export default ClaudeCodeRLMPlugin
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin types for Claude Code integration.
|
|
3
|
+
*
|
|
4
|
+
* These types mirror the @claudecode/plugin package interface.
|
|
5
|
+
* When @claudecode/plugin is available, its types take precedence.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Plugin input provided by Claude Code.
|
|
12
|
+
*/
|
|
13
|
+
export interface PluginInput {
|
|
14
|
+
client: any
|
|
15
|
+
project: any
|
|
16
|
+
directory: string
|
|
17
|
+
worktree: string
|
|
18
|
+
serverUrl: URL
|
|
19
|
+
$: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Event from Claude Code.
|
|
24
|
+
*/
|
|
25
|
+
export interface Event {
|
|
26
|
+
type: string
|
|
27
|
+
properties: Record<string, any>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Message types.
|
|
32
|
+
*/
|
|
33
|
+
export interface Message {
|
|
34
|
+
id: string
|
|
35
|
+
sessionID: string
|
|
36
|
+
role: "user" | "assistant" | "system"
|
|
37
|
+
time: number
|
|
38
|
+
agent?: string
|
|
39
|
+
model?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Part {
|
|
43
|
+
type: string
|
|
44
|
+
[key: string]: any
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Plugin hooks interface.
|
|
49
|
+
*/
|
|
50
|
+
export interface Hooks {
|
|
51
|
+
event?: (input: { event: Event }) => Promise<void>
|
|
52
|
+
config?: (input: any) => Promise<void>
|
|
53
|
+
tool?: Record<string, ToolDefinition>
|
|
54
|
+
"chat.params"?: (input: any, output: any) => Promise<void>
|
|
55
|
+
"experimental.chat.messages.transform"?: (
|
|
56
|
+
input: {},
|
|
57
|
+
output: {
|
|
58
|
+
messages: Array<{
|
|
59
|
+
info: Message
|
|
60
|
+
parts: Part[]
|
|
61
|
+
}>
|
|
62
|
+
}
|
|
63
|
+
) => Promise<void>
|
|
64
|
+
"experimental.session.compacting"?: (
|
|
65
|
+
input: { sessionID: string },
|
|
66
|
+
output: { context: string[]; prompt?: string }
|
|
67
|
+
) => Promise<void>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Plugin function type.
|
|
72
|
+
*/
|
|
73
|
+
export type Plugin = (input: PluginInput) => Promise<Hooks>
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Tool definition interface.
|
|
77
|
+
*/
|
|
78
|
+
export interface ToolDefinition {
|
|
79
|
+
description: string
|
|
80
|
+
args: Record<string, z.ZodType>
|
|
81
|
+
execute: (args: any, context: ToolContext) => Promise<ToolResult>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ToolContext {
|
|
85
|
+
sessionID: string
|
|
86
|
+
abortSignal?: AbortSignal
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ToolResult {
|
|
90
|
+
title: string
|
|
91
|
+
output: string
|
|
92
|
+
metadata?: Record<string, any>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Helper to create tool definitions.
|
|
97
|
+
*/
|
|
98
|
+
export const tool = {
|
|
99
|
+
schema: z,
|
|
100
|
+
define(def: {
|
|
101
|
+
description: string
|
|
102
|
+
args: Record<string, z.ZodType>
|
|
103
|
+
execute: (args: any, context: ToolContext) => Promise<ToolResult>
|
|
104
|
+
}): ToolDefinition {
|
|
105
|
+
return def
|
|
106
|
+
},
|
|
107
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced hybrid search with recency weighting and entity boost.
|
|
3
|
+
*
|
|
4
|
+
* Combines keyword search, entity matching, and graph traversal
|
|
5
|
+
* with configurable scoring weights.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from "zod"
|
|
9
|
+
import { GraphStorage, GraphSearcher, type GraphNode, type GraphSearchResult, NodeType } from "../graph/index.js"
|
|
10
|
+
import { detectsReference, getMatchingPatterns } from "./patterns.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Enhanced search configuration schema.
|
|
14
|
+
*/
|
|
15
|
+
export const EnhancedSearchConfigSchema = z.object({
|
|
16
|
+
/** Use additional reference patterns */
|
|
17
|
+
additionalPatterns: z.boolean().default(true),
|
|
18
|
+
/** Weight for recency in scoring (0-1) */
|
|
19
|
+
recencyWeight: z.number().min(0).max(1).default(0.3),
|
|
20
|
+
/** Weight for entity matches in scoring (0-1) */
|
|
21
|
+
entityBoost: z.number().min(0).max(1).default(0.2),
|
|
22
|
+
/** Maximum age in days for recency scoring */
|
|
23
|
+
maxAgeDays: z.number().default(30),
|
|
24
|
+
/** Minimum score threshold for results */
|
|
25
|
+
minScoreThreshold: z.number().default(0.3),
|
|
26
|
+
/** Maximum results to return */
|
|
27
|
+
limit: z.number().default(10),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export type EnhancedSearchConfig = z.infer<typeof EnhancedSearchConfigSchema>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Enhanced search result with additional metadata.
|
|
34
|
+
*/
|
|
35
|
+
export interface EnhancedSearchResult extends GraphSearchResult {
|
|
36
|
+
recencyScore: number
|
|
37
|
+
entityScore: number
|
|
38
|
+
finalScore: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Injection analysis result.
|
|
43
|
+
*/
|
|
44
|
+
export interface InjectionAnalysis {
|
|
45
|
+
shouldInject: boolean
|
|
46
|
+
matchedPatterns: RegExp[]
|
|
47
|
+
results: EnhancedSearchResult[]
|
|
48
|
+
query: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Enhanced search namespace.
|
|
53
|
+
*/
|
|
54
|
+
export namespace EnhancedSearch {
|
|
55
|
+
const DEFAULT_CONFIG: EnhancedSearchConfig = {
|
|
56
|
+
additionalPatterns: true,
|
|
57
|
+
recencyWeight: 0.3,
|
|
58
|
+
entityBoost: 0.2,
|
|
59
|
+
maxAgeDays: 30,
|
|
60
|
+
minScoreThreshold: 0.3,
|
|
61
|
+
limit: 10,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if context should be retrieved for a message.
|
|
66
|
+
*/
|
|
67
|
+
export function shouldRetrieve(
|
|
68
|
+
message: string,
|
|
69
|
+
config: Partial<EnhancedSearchConfig> = {}
|
|
70
|
+
): { shouldRetrieve: boolean; patterns: RegExp[] } {
|
|
71
|
+
const patterns = getMatchingPatterns(message)
|
|
72
|
+
return {
|
|
73
|
+
shouldRetrieve: patterns.length > 0,
|
|
74
|
+
patterns,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Calculate recency score for a node.
|
|
80
|
+
* Score decreases as the node ages.
|
|
81
|
+
*/
|
|
82
|
+
export function calculateRecencyScore(
|
|
83
|
+
nodeCreatedAt: number,
|
|
84
|
+
maxAgeDays: number = 30
|
|
85
|
+
): number {
|
|
86
|
+
const now = Date.now()
|
|
87
|
+
const ageMs = now - nodeCreatedAt
|
|
88
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24)
|
|
89
|
+
|
|
90
|
+
if (ageDays <= 0) return 1.0
|
|
91
|
+
if (ageDays >= maxAgeDays) return 0.0
|
|
92
|
+
|
|
93
|
+
// Linear decay
|
|
94
|
+
return 1.0 - ageDays / maxAgeDays
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Calculate keyword match score.
|
|
99
|
+
*/
|
|
100
|
+
export function calculateKeywordScore(
|
|
101
|
+
content: string,
|
|
102
|
+
keywords: string[]
|
|
103
|
+
): number {
|
|
104
|
+
if (keywords.length === 0) return 0
|
|
105
|
+
|
|
106
|
+
const contentLower = content.toLowerCase()
|
|
107
|
+
let matches = 0
|
|
108
|
+
|
|
109
|
+
for (const keyword of keywords) {
|
|
110
|
+
if (contentLower.includes(keyword.toLowerCase())) {
|
|
111
|
+
matches++
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return matches / keywords.length
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Search with enhanced scoring.
|
|
120
|
+
*/
|
|
121
|
+
export function search(
|
|
122
|
+
sessionID: string,
|
|
123
|
+
query: string,
|
|
124
|
+
config: Partial<EnhancedSearchConfig> = {}
|
|
125
|
+
): EnhancedSearchResult[] {
|
|
126
|
+
const cfg = { ...DEFAULT_CONFIG, ...config }
|
|
127
|
+
|
|
128
|
+
// Get base results from graph searcher
|
|
129
|
+
const baseResults = GraphSearcher.search(sessionID, query, {
|
|
130
|
+
limit: cfg.limit * 2, // Get extra for filtering
|
|
131
|
+
expandContext: true,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Extract keywords from query
|
|
135
|
+
const keywords = query
|
|
136
|
+
.toLowerCase()
|
|
137
|
+
.split(/\s+/)
|
|
138
|
+
.filter((w) => w.length > 2)
|
|
139
|
+
|
|
140
|
+
// Enhance with additional scoring
|
|
141
|
+
const enhanced: EnhancedSearchResult[] = baseResults.map((result) => {
|
|
142
|
+
const recencyScore = calculateRecencyScore(
|
|
143
|
+
result.node.createdAt,
|
|
144
|
+
cfg.maxAgeDays
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const entityScore =
|
|
148
|
+
result.matchedOn === "entity" ? cfg.entityBoost : 0
|
|
149
|
+
|
|
150
|
+
const keywordScore = calculateKeywordScore(result.node.content, keywords)
|
|
151
|
+
|
|
152
|
+
// Combine scores
|
|
153
|
+
const baseScore = result.score
|
|
154
|
+
const finalScore =
|
|
155
|
+
baseScore * (1 - cfg.recencyWeight) +
|
|
156
|
+
recencyScore * cfg.recencyWeight +
|
|
157
|
+
entityScore +
|
|
158
|
+
keywordScore * 0.1
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
...result,
|
|
162
|
+
recencyScore,
|
|
163
|
+
entityScore,
|
|
164
|
+
finalScore,
|
|
165
|
+
score: finalScore,
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Filter by threshold and sort
|
|
170
|
+
return enhanced
|
|
171
|
+
.filter((r) => r.finalScore >= cfg.minScoreThreshold)
|
|
172
|
+
.sort((a, b) => b.finalScore - a.finalScore)
|
|
173
|
+
.slice(0, cfg.limit)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Analyze a message and determine if context should be injected.
|
|
178
|
+
*/
|
|
179
|
+
export function analyzeForInjection(
|
|
180
|
+
sessionID: string,
|
|
181
|
+
message: string,
|
|
182
|
+
config: Partial<EnhancedSearchConfig> = {}
|
|
183
|
+
): InjectionAnalysis {
|
|
184
|
+
const { shouldRetrieve: should, patterns } = shouldRetrieve(message, config)
|
|
185
|
+
|
|
186
|
+
if (!should) {
|
|
187
|
+
return {
|
|
188
|
+
shouldInject: false,
|
|
189
|
+
matchedPatterns: [],
|
|
190
|
+
results: [],
|
|
191
|
+
query: message,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Search for relevant context
|
|
196
|
+
const results = search(sessionID, message, config)
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
shouldInject: results.length > 0,
|
|
200
|
+
matchedPatterns: patterns,
|
|
201
|
+
results,
|
|
202
|
+
query: message,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Format search results for injection into context.
|
|
208
|
+
*/
|
|
209
|
+
export function formatForInjection(results: EnhancedSearchResult[]): string {
|
|
210
|
+
if (results.length === 0) {
|
|
211
|
+
return ""
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const sections: string[] = [
|
|
215
|
+
"<retrieved_context>",
|
|
216
|
+
"The following relevant context was retrieved from previous conversations:",
|
|
217
|
+
"",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < results.length; i++) {
|
|
221
|
+
const result = results[i]
|
|
222
|
+
const node = result.node
|
|
223
|
+
|
|
224
|
+
// Format based on node type
|
|
225
|
+
if (node.type === "chunk") {
|
|
226
|
+
sections.push(`[Context ${i + 1}]`)
|
|
227
|
+
sections.push(node.content)
|
|
228
|
+
sections.push("")
|
|
229
|
+
} else if (node.type === "document") {
|
|
230
|
+
sections.push(`[Document: ${node.content}]`)
|
|
231
|
+
if (node.metadata?.fullContent) {
|
|
232
|
+
sections.push(node.metadata.fullContent)
|
|
233
|
+
}
|
|
234
|
+
sections.push("")
|
|
235
|
+
} else if (node.type === "entity") {
|
|
236
|
+
sections.push(`[Entity: ${node.content}]`)
|
|
237
|
+
sections.push("")
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
sections.push("</retrieved_context>")
|
|
242
|
+
|
|
243
|
+
return sections.join("\n")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get entity suggestions for a query.
|
|
248
|
+
*/
|
|
249
|
+
export function suggestEntities(
|
|
250
|
+
sessionID: string,
|
|
251
|
+
query: string,
|
|
252
|
+
limit: number = 5
|
|
253
|
+
): GraphNode[] {
|
|
254
|
+
// Search for entities matching the query
|
|
255
|
+
const entities = GraphStorage.searchNodes(
|
|
256
|
+
sessionID,
|
|
257
|
+
query,
|
|
258
|
+
NodeType.ENTITY,
|
|
259
|
+
limit
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
return entities
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search module exports.
|
|
3
|
+
*
|
|
4
|
+
* Extended reference patterns and enhanced hybrid search.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
EXPLICIT_MEMORY_PATTERNS,
|
|
9
|
+
EXTENDED_PATTERNS,
|
|
10
|
+
TASK_COMPLETION_PATTERNS,
|
|
11
|
+
ALL_PATTERNS,
|
|
12
|
+
detectsReference,
|
|
13
|
+
getMatchingPatterns,
|
|
14
|
+
detectsTaskCompletion,
|
|
15
|
+
} from "./patterns.js"
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
EnhancedSearch,
|
|
19
|
+
EnhancedSearchConfigSchema,
|
|
20
|
+
type EnhancedSearchConfig,
|
|
21
|
+
type EnhancedSearchResult,
|
|
22
|
+
type InjectionAnalysis,
|
|
23
|
+
} from "./enhanced.js"
|