@vainplex/openclaw-knowledge-engine 0.1.1 → 0.1.3
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 +1 -1
- package/README.md +7 -7
- package/dist/index.d.ts +8 -4
- package/dist/index.js +31 -27
- package/dist/src/config-loader.d.ts +22 -0
- package/dist/src/config-loader.js +104 -0
- package/dist/src/config.d.ts +1 -1
- package/dist/src/config.js +3 -2
- package/openclaw.plugin.json +117 -113
- package/package.json +15 -5
- package/ARCHITECTURE.md +0 -374
- package/index.ts +0 -38
- package/src/config.ts +0 -180
- package/src/embeddings.ts +0 -82
- package/src/entity-extractor.ts +0 -137
- package/src/fact-store.ts +0 -260
- package/src/hooks.ts +0 -125
- package/src/http-client.ts +0 -74
- package/src/llm-enhancer.ts +0 -187
- package/src/maintenance.ts +0 -102
- package/src/patterns.ts +0 -90
- package/src/storage.ts +0 -122
- package/src/types.ts +0 -144
- package/test/config.test.ts +0 -152
- package/test/embeddings.test.ts +0 -118
- package/test/entity-extractor.test.ts +0 -121
- package/test/fact-store.test.ts +0 -266
- package/test/hooks.test.ts +0 -120
- package/test/http-client.test.ts +0 -68
- package/test/llm-enhancer.test.ts +0 -132
- package/test/maintenance.test.ts +0 -117
- package/test/patterns.test.ts +0 -123
- package/test/storage.test.ts +0 -86
- package/tsconfig.json +0 -26
package/src/entity-extractor.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// src/entity-extractor.ts
|
|
2
|
-
|
|
3
|
-
import { Entity, Logger } from './types.js';
|
|
4
|
-
import { REGEX_PATTERNS } from './patterns.js';
|
|
5
|
-
|
|
6
|
-
// A map to associate regex pattern names with entity types.
|
|
7
|
-
const PATTERN_TYPE_MAP: Record<string, Entity['type']> = {
|
|
8
|
-
email: 'email',
|
|
9
|
-
url: 'url',
|
|
10
|
-
iso_date: 'date',
|
|
11
|
-
common_date: 'date',
|
|
12
|
-
german_date: 'date',
|
|
13
|
-
english_date: 'date',
|
|
14
|
-
proper_noun: 'unknown',
|
|
15
|
-
product_name: 'product',
|
|
16
|
-
organization_suffix: 'organization',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Extracts entities from text using predefined regular expressions.
|
|
21
|
-
*/
|
|
22
|
-
export class EntityExtractor {
|
|
23
|
-
private readonly logger: Logger;
|
|
24
|
-
|
|
25
|
-
constructor(logger: Logger) {
|
|
26
|
-
this.logger = logger;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Extracts entities from a given text based on the regex patterns.
|
|
31
|
-
* @param text The input text to process.
|
|
32
|
-
* @returns An array of found entities.
|
|
33
|
-
*/
|
|
34
|
-
public extract(text: string): Entity[] {
|
|
35
|
-
const foundEntities: Map<string, Entity> = new Map();
|
|
36
|
-
|
|
37
|
-
for (const key in REGEX_PATTERNS) {
|
|
38
|
-
// Each access returns a fresh RegExp (via Proxy), avoiding /g state-bleed.
|
|
39
|
-
const regex = REGEX_PATTERNS[key];
|
|
40
|
-
if (!regex.global) {
|
|
41
|
-
this.logger.warn(`Regex for "${key}" is not global. Skipping.`);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
const entityType = PATTERN_TYPE_MAP[key] || 'unknown';
|
|
45
|
-
let match;
|
|
46
|
-
while ((match = regex.exec(text)) !== null) {
|
|
47
|
-
const value = match[0].trim();
|
|
48
|
-
if (!value) continue;
|
|
49
|
-
this.processMatch(key, value, entityType, foundEntities);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return Array.from(foundEntities.values());
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Processes a single regex match and upserts it into the entity map.
|
|
58
|
-
*/
|
|
59
|
-
private processMatch(
|
|
60
|
-
_key: string,
|
|
61
|
-
value: string,
|
|
62
|
-
entityType: Entity['type'],
|
|
63
|
-
entities: Map<string, Entity>
|
|
64
|
-
): void {
|
|
65
|
-
const canonicalValue = this.canonicalize(value, entityType);
|
|
66
|
-
const id = `${entityType}:${canonicalValue.toLowerCase().replace(/\s+/g, '-')}`;
|
|
67
|
-
|
|
68
|
-
if (entities.has(id)) {
|
|
69
|
-
const existing = entities.get(id)!;
|
|
70
|
-
if (!existing.mentions.includes(value)) existing.mentions.push(value);
|
|
71
|
-
existing.count++;
|
|
72
|
-
if (!existing.source.includes('regex')) existing.source.push('regex');
|
|
73
|
-
} else {
|
|
74
|
-
entities.set(id, {
|
|
75
|
-
id,
|
|
76
|
-
type: entityType,
|
|
77
|
-
value: canonicalValue,
|
|
78
|
-
mentions: [value],
|
|
79
|
-
count: 1,
|
|
80
|
-
importance: this.calculateInitialImportance(entityType, value),
|
|
81
|
-
lastSeen: new Date().toISOString(),
|
|
82
|
-
source: ['regex'],
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Cleans and standardizes an entity value based on its type.
|
|
89
|
-
*/
|
|
90
|
-
private canonicalize(value: string, type: Entity['type']): string {
|
|
91
|
-
if (type === 'organization') {
|
|
92
|
-
const suffixes = /,?\s?(?:Inc\.|LLC|Corp\.|GmbH|AG|Ltd\.)$/i;
|
|
93
|
-
return value.replace(suffixes, '').trim();
|
|
94
|
-
}
|
|
95
|
-
return value.replace(/[.,!?;:]$/, '').trim();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Calculates an initial importance score for an entity.
|
|
100
|
-
*/
|
|
101
|
-
private calculateInitialImportance(type: Entity['type'], value: string): number {
|
|
102
|
-
switch (type) {
|
|
103
|
-
case 'organization': return 0.8;
|
|
104
|
-
case 'person': return 0.7;
|
|
105
|
-
case 'product': return 0.6;
|
|
106
|
-
case 'location': return 0.5;
|
|
107
|
-
case 'date':
|
|
108
|
-
case 'email':
|
|
109
|
-
case 'url': return 0.4;
|
|
110
|
-
default: return value.split(/\s|-/).length > 1 ? 0.5 : 0.3;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Merges two lists of entities by ID.
|
|
116
|
-
*/
|
|
117
|
-
public static mergeEntities(listA: Entity[], listB: Entity[]): Entity[] {
|
|
118
|
-
const merged: Map<string, Entity> = new Map();
|
|
119
|
-
for (const e of listA) merged.set(e.id, { ...e });
|
|
120
|
-
|
|
121
|
-
for (const entity of listB) {
|
|
122
|
-
if (merged.has(entity.id)) {
|
|
123
|
-
const ex = merged.get(entity.id)!;
|
|
124
|
-
ex.count += entity.count;
|
|
125
|
-
ex.mentions = [...new Set([...ex.mentions, ...entity.mentions])];
|
|
126
|
-
ex.source = [...new Set([...ex.source, ...entity.source])];
|
|
127
|
-
ex.lastSeen = new Date() > new Date(ex.lastSeen)
|
|
128
|
-
? new Date().toISOString() : ex.lastSeen;
|
|
129
|
-
ex.importance = Math.max(ex.importance, entity.importance);
|
|
130
|
-
} else {
|
|
131
|
-
merged.set(entity.id, { ...entity });
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return Array.from(merged.values());
|
|
136
|
-
}
|
|
137
|
-
}
|
package/src/fact-store.ts
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
// src/fact-store.ts
|
|
2
|
-
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { AtomicStorage } from './storage.js';
|
|
5
|
-
import type { Fact, FactsData, KnowledgeConfig, Logger } from './types.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Manages an in-memory and on-disk store of structured facts.
|
|
9
|
-
* Provides methods for loading, querying, modifying, and persisting facts.
|
|
10
|
-
*/
|
|
11
|
-
export class FactStore {
|
|
12
|
-
private readonly storage: AtomicStorage;
|
|
13
|
-
private readonly config: KnowledgeConfig['storage'];
|
|
14
|
-
private readonly logger: Logger;
|
|
15
|
-
private facts: Map<string, Fact> = new Map();
|
|
16
|
-
private isLoaded: boolean = false;
|
|
17
|
-
|
|
18
|
-
public readonly commit: () => Promise<void>;
|
|
19
|
-
|
|
20
|
-
constructor(
|
|
21
|
-
workspace: string,
|
|
22
|
-
config: KnowledgeConfig['storage'],
|
|
23
|
-
logger: Logger
|
|
24
|
-
) {
|
|
25
|
-
this.storage = new AtomicStorage(workspace, logger);
|
|
26
|
-
this.config = config;
|
|
27
|
-
this.logger = logger;
|
|
28
|
-
|
|
29
|
-
// Create a debounced version of the persist method.
|
|
30
|
-
this.commit = AtomicStorage.debounce(
|
|
31
|
-
this.persist.bind(this),
|
|
32
|
-
this.config.writeDebounceMs
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Immediately flushes any pending debounced writes.
|
|
38
|
-
* Useful in tests and before shutdown to ensure data is persisted.
|
|
39
|
-
*/
|
|
40
|
-
public async flush(): Promise<void> {
|
|
41
|
-
if (this.isLoaded) {
|
|
42
|
-
await this.persist();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Loads facts from the `facts.json` file into the in-memory store.
|
|
48
|
-
* If the file doesn't exist, it initializes an empty store.
|
|
49
|
-
*/
|
|
50
|
-
public async load(): Promise<void> {
|
|
51
|
-
if (this.isLoaded) {
|
|
52
|
-
this.logger.debug('Fact store is already loaded.');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
await this.storage.init();
|
|
57
|
-
const data = await this.storage.readJson<FactsData>('facts.json');
|
|
58
|
-
if (data && Array.isArray(data.facts)) {
|
|
59
|
-
this.facts = new Map(data.facts.map(fact => [fact.id, fact]));
|
|
60
|
-
this.logger.info(`Loaded ${this.facts.size} facts from storage.`);
|
|
61
|
-
} else {
|
|
62
|
-
this.logger.info('No existing fact store found. Initializing a new one.');
|
|
63
|
-
this.facts = new Map();
|
|
64
|
-
}
|
|
65
|
-
this.isLoaded = true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Adds a new fact to the store or updates an existing one based on content.
|
|
70
|
-
* @param newFactData The data for the new fact, excluding metadata fields.
|
|
71
|
-
* @returns The newly created or found Fact object.
|
|
72
|
-
*/
|
|
73
|
-
public addFact(
|
|
74
|
-
newFactData: Omit<Fact, 'id' | 'createdAt' | 'lastAccessed' | 'relevance'>
|
|
75
|
-
): Fact {
|
|
76
|
-
if (!this.isLoaded) {
|
|
77
|
-
throw new Error('FactStore has not been loaded yet. Call load() first.');
|
|
78
|
-
}
|
|
79
|
-
const now = new Date().toISOString();
|
|
80
|
-
|
|
81
|
-
// Check if a similar fact already exists to avoid duplicates
|
|
82
|
-
for (const existingFact of this.facts.values()) {
|
|
83
|
-
if (
|
|
84
|
-
existingFact.subject === newFactData.subject &&
|
|
85
|
-
existingFact.predicate === newFactData.predicate &&
|
|
86
|
-
existingFact.object === newFactData.object
|
|
87
|
-
) {
|
|
88
|
-
// Fact already exists, let's just boost its relevance and update timestamp
|
|
89
|
-
existingFact.relevance = this.boostRelevance(existingFact.relevance);
|
|
90
|
-
existingFact.lastAccessed = now;
|
|
91
|
-
this.commit();
|
|
92
|
-
return existingFact;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const newFact: Fact = {
|
|
97
|
-
...newFactData,
|
|
98
|
-
id: randomUUID(),
|
|
99
|
-
createdAt: now,
|
|
100
|
-
lastAccessed: now,
|
|
101
|
-
relevance: 1.0, // New facts start with maximum relevance
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
this.facts.set(newFact.id, newFact);
|
|
105
|
-
this.prune(); // Check if we need to prune old facts
|
|
106
|
-
this.commit();
|
|
107
|
-
return newFact;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Retrieves a fact by its unique ID.
|
|
112
|
-
* @param id The UUID of the fact.
|
|
113
|
-
* @returns The Fact object, or undefined if not found.
|
|
114
|
-
*/
|
|
115
|
-
public getFact(id: string): Fact | undefined {
|
|
116
|
-
const fact = this.facts.get(id);
|
|
117
|
-
if (fact) {
|
|
118
|
-
fact.lastAccessed = new Date().toISOString();
|
|
119
|
-
fact.relevance = this.boostRelevance(fact.relevance);
|
|
120
|
-
this.commit();
|
|
121
|
-
}
|
|
122
|
-
return fact;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Queries the fact store based on subject, predicate, or object.
|
|
127
|
-
* @param query An object with optional subject, predicate, and/or object to match.
|
|
128
|
-
* @returns An array of matching facts, sorted by relevance.
|
|
129
|
-
*/
|
|
130
|
-
public query(query: { subject?: string; predicate?: string; object?: string }): Fact[] {
|
|
131
|
-
const results: Fact[] = [];
|
|
132
|
-
for (const fact of this.facts.values()) {
|
|
133
|
-
const subjectMatch = !query.subject || fact.subject === query.subject;
|
|
134
|
-
const predicateMatch = !query.predicate || fact.predicate === query.predicate;
|
|
135
|
-
const objectMatch = !query.object || fact.object === query.object;
|
|
136
|
-
|
|
137
|
-
if (subjectMatch && predicateMatch && objectMatch) {
|
|
138
|
-
results.push(fact);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
// Sort by relevance, descending
|
|
142
|
-
return results.sort((a, b) => b.relevance - a.relevance);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Applies a decay factor to the relevance score of all facts.
|
|
147
|
-
* @param rate The decay rate (e.g., 0.05 for 5%).
|
|
148
|
-
* @returns An object with the count of decayed facts.
|
|
149
|
-
*/
|
|
150
|
-
public decayFacts(rate: number): { decayedCount: number } {
|
|
151
|
-
let decayedCount = 0;
|
|
152
|
-
const minRelevance = 0.1; // Floor to prevent facts from disappearing completely
|
|
153
|
-
|
|
154
|
-
for (const fact of this.facts.values()) {
|
|
155
|
-
const newRelevance = fact.relevance * (1 - rate);
|
|
156
|
-
if (newRelevance !== fact.relevance) {
|
|
157
|
-
fact.relevance = Math.max(minRelevance, newRelevance);
|
|
158
|
-
decayedCount++;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (decayedCount > 0) {
|
|
163
|
-
this.logger.info(`Applied decay rate of ${rate * 100}% to ${decayedCount} facts.`);
|
|
164
|
-
this.commit();
|
|
165
|
-
}
|
|
166
|
-
return { decayedCount };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Persists the current in-memory fact store to `facts.json`.
|
|
171
|
-
*/
|
|
172
|
-
private async persist(): Promise<void> {
|
|
173
|
-
if (!this.isLoaded) {
|
|
174
|
-
this.logger.warn('Attempted to persist fact store before it was loaded. Aborting.');
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const data: FactsData = {
|
|
179
|
-
updated: new Date().toISOString(),
|
|
180
|
-
facts: Array.from(this.facts.values()),
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
await this.storage.writeJson('facts.json', data);
|
|
185
|
-
this.logger.debug(`Successfully persisted ${data.facts.length} facts.`);
|
|
186
|
-
} catch (err) {
|
|
187
|
-
this.logger.error('Failed to persist fact store.', err as Error);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Removes the least relevant facts if the store exceeds its configured max size.
|
|
193
|
-
*/
|
|
194
|
-
private prune(): void {
|
|
195
|
-
const factCount = this.facts.size;
|
|
196
|
-
if (factCount <= this.config.maxFacts) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const factsToPrune = factCount - this.config.maxFacts;
|
|
201
|
-
if (factsToPrune <= 0) return;
|
|
202
|
-
|
|
203
|
-
// Get all facts, sort by relevance (ascending) and then by lastAccessed (ascending)
|
|
204
|
-
const sortedFacts = Array.from(this.facts.values()).sort((a, b) => {
|
|
205
|
-
if (a.relevance !== b.relevance) {
|
|
206
|
-
return a.relevance - b.relevance;
|
|
207
|
-
}
|
|
208
|
-
return new Date(a.lastAccessed).getTime() - new Date(b.lastAccessed).getTime();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
for (let i = 0; i < factsToPrune; i++) {
|
|
212
|
-
this.facts.delete(sortedFacts[i].id);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
this.logger.info(`Pruned ${factsToPrune} least relevant facts to maintain store size.`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Boosts the relevance of a fact upon access.
|
|
220
|
-
* @param currentRelevance The current relevance score.
|
|
221
|
-
* @returns The new, boosted relevance score.
|
|
222
|
-
*/
|
|
223
|
-
private boostRelevance(currentRelevance: number): number {
|
|
224
|
-
// Push the relevance 50% closer to 1.0
|
|
225
|
-
const boost = (1.0 - currentRelevance) * 0.5;
|
|
226
|
-
return Math.min(1.0, currentRelevance + boost);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Returns a list of all facts that have not been embedded yet.
|
|
231
|
-
*/
|
|
232
|
-
public getUnembeddedFacts(): Fact[] {
|
|
233
|
-
const results: Fact[] = [];
|
|
234
|
-
for (const fact of this.facts.values()) {
|
|
235
|
-
if (!fact.embedded) {
|
|
236
|
-
results.push(fact);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return results;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Marks a list of facts as having been embedded.
|
|
244
|
-
* @param factIds An array of fact IDs to update.
|
|
245
|
-
*/
|
|
246
|
-
public markFactsAsEmbedded(factIds: string[]): void {
|
|
247
|
-
const now = new Date().toISOString();
|
|
248
|
-
let updatedCount = 0;
|
|
249
|
-
for (const id of factIds) {
|
|
250
|
-
const fact = this.facts.get(id);
|
|
251
|
-
if (fact) {
|
|
252
|
-
fact.embedded = now;
|
|
253
|
-
updatedCount++;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
if (updatedCount > 0) {
|
|
257
|
-
this.commit();
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
package/src/hooks.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// src/hooks.ts
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
OpenClawPluginApi,
|
|
5
|
-
HookEvent,
|
|
6
|
-
KnowledgeConfig,
|
|
7
|
-
Logger,
|
|
8
|
-
} from './types.js';
|
|
9
|
-
|
|
10
|
-
import { EntityExtractor } from './entity-extractor.js';
|
|
11
|
-
import { FactStore } from './fact-store.js';
|
|
12
|
-
import { LlmEnhancer } from './llm-enhancer.js';
|
|
13
|
-
import { Maintenance } from './maintenance.js';
|
|
14
|
-
import { Embeddings } from './embeddings.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Manages the registration and orchestration of all plugin hooks.
|
|
18
|
-
*/
|
|
19
|
-
export class HookManager {
|
|
20
|
-
private readonly api: OpenClawPluginApi;
|
|
21
|
-
private readonly config: KnowledgeConfig;
|
|
22
|
-
private readonly logger: Logger;
|
|
23
|
-
|
|
24
|
-
private entityExtractor: EntityExtractor;
|
|
25
|
-
private factStore: FactStore;
|
|
26
|
-
private llmEnhancer?: LlmEnhancer;
|
|
27
|
-
private maintenance?: Maintenance;
|
|
28
|
-
|
|
29
|
-
constructor(api: OpenClawPluginApi, config: KnowledgeConfig) {
|
|
30
|
-
this.api = api;
|
|
31
|
-
this.config = config;
|
|
32
|
-
this.logger = api.logger;
|
|
33
|
-
|
|
34
|
-
this.entityExtractor = new EntityExtractor(this.logger);
|
|
35
|
-
this.factStore = new FactStore(
|
|
36
|
-
this.config.workspace,
|
|
37
|
-
this.config.storage,
|
|
38
|
-
this.logger
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
if (this.config.extraction.llm.enabled) {
|
|
42
|
-
this.llmEnhancer = new LlmEnhancer(this.config.extraction.llm, this.logger);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Registers all the necessary hooks with the OpenClaw host. */
|
|
47
|
-
public registerHooks(): void {
|
|
48
|
-
if (!this.config.enabled) {
|
|
49
|
-
this.logger.info('Knowledge Engine is disabled. No hooks registered.');
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
this.api.on('session_start', this.onSessionStart.bind(this), { priority: 200 });
|
|
54
|
-
this.api.on('message_received', this.onMessage.bind(this), { priority: 100 });
|
|
55
|
-
this.api.on('message_sent', this.onMessage.bind(this), { priority: 100 });
|
|
56
|
-
this.api.on('gateway_stop', this.onShutdown.bind(this), { priority: 900 });
|
|
57
|
-
|
|
58
|
-
this.logger.info('Registered all Knowledge Engine hooks.');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Handler for the `session_start` hook. */
|
|
62
|
-
private async onSessionStart(): Promise<void> {
|
|
63
|
-
this.logger.info('Knowledge Engine starting up...');
|
|
64
|
-
await this.factStore.load();
|
|
65
|
-
|
|
66
|
-
const embeddings = this.config.embeddings.enabled
|
|
67
|
-
? new Embeddings(this.config.embeddings, this.logger)
|
|
68
|
-
: undefined;
|
|
69
|
-
|
|
70
|
-
this.maintenance = new Maintenance(
|
|
71
|
-
this.config, this.logger, this.factStore, embeddings
|
|
72
|
-
);
|
|
73
|
-
this.maintenance.start();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Handler for `gateway_stop` — cleans up timers and flushes state. */
|
|
77
|
-
private async onShutdown(): Promise<void> {
|
|
78
|
-
this.logger.info('Knowledge Engine shutting down...');
|
|
79
|
-
this.maintenance?.stop();
|
|
80
|
-
this.llmEnhancer?.clearTimers();
|
|
81
|
-
this.logger.info('Knowledge Engine shutdown complete.');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Handler for `message_received` and `message_sent` hooks. */
|
|
85
|
-
private async onMessage(event: HookEvent): Promise<void> {
|
|
86
|
-
const text = event.content || event.message || event.text;
|
|
87
|
-
if (typeof text !== 'string' || text.trim() === '') return;
|
|
88
|
-
|
|
89
|
-
this.logger.debug(`Processing message: "${text.substring(0, 50)}..."`);
|
|
90
|
-
|
|
91
|
-
if (this.config.extraction.regex.enabled) {
|
|
92
|
-
const entities = this.entityExtractor.extract(text);
|
|
93
|
-
if (entities.length > 0) {
|
|
94
|
-
this.logger.info(`Extracted ${entities.length} entities via regex.`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (this.llmEnhancer) {
|
|
99
|
-
const messageId = `msg-${Date.now()}`;
|
|
100
|
-
this.llmEnhancer.addToBatch({ id: messageId, text });
|
|
101
|
-
this.processLlmBatchWhenReady().catch(err =>
|
|
102
|
-
this.logger.error('LLM batch processing failed', err as Error));
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Fire-and-forget: processes LLM batch results when available. */
|
|
107
|
-
private async processLlmBatchWhenReady(): Promise<void> {
|
|
108
|
-
if (!this.llmEnhancer) return;
|
|
109
|
-
|
|
110
|
-
const result = await this.llmEnhancer.sendBatch();
|
|
111
|
-
if (!result) return;
|
|
112
|
-
|
|
113
|
-
if (result.facts.length > 0) {
|
|
114
|
-
this.logger.info(`Adding ${result.facts.length} LLM facts.`);
|
|
115
|
-
for (const f of result.facts) {
|
|
116
|
-
this.factStore.addFact({
|
|
117
|
-
subject: f.subject,
|
|
118
|
-
predicate: f.predicate,
|
|
119
|
-
object: f.object,
|
|
120
|
-
source: 'extracted-llm',
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
package/src/http-client.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// src/http-client.ts
|
|
2
|
-
|
|
3
|
-
import * as http from 'node:http';
|
|
4
|
-
import * as https from 'node:https';
|
|
5
|
-
|
|
6
|
-
interface HttpPostOptions {
|
|
7
|
-
hostname: string;
|
|
8
|
-
port: string;
|
|
9
|
-
path: string;
|
|
10
|
-
method: 'POST';
|
|
11
|
-
headers: Record<string, string | number>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Selects the correct HTTP/HTTPS module based on the URL protocol.
|
|
16
|
-
*/
|
|
17
|
-
function selectTransport(protocol: string): typeof http | typeof https {
|
|
18
|
-
return protocol === 'https:' ? https : http;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Builds request options from a URL and payload.
|
|
23
|
-
*/
|
|
24
|
-
function buildRequestOptions(url: URL, payload: string): HttpPostOptions {
|
|
25
|
-
return {
|
|
26
|
-
hostname: url.hostname,
|
|
27
|
-
port: url.port,
|
|
28
|
-
path: url.pathname + url.search,
|
|
29
|
-
method: 'POST',
|
|
30
|
-
headers: {
|
|
31
|
-
'Content-Type': 'application/json',
|
|
32
|
-
'Content-Length': Buffer.byteLength(payload),
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Makes an HTTP or HTTPS POST request, auto-selecting the
|
|
39
|
-
* transport based on the URL's protocol.
|
|
40
|
-
*
|
|
41
|
-
* @param url The full URL string to POST to.
|
|
42
|
-
* @param body The payload object to JSON-serialize and send.
|
|
43
|
-
* @returns A promise resolving with the response body string.
|
|
44
|
-
*/
|
|
45
|
-
export function httpPost(url: string, body: unknown): Promise<string> {
|
|
46
|
-
return new Promise((resolve, reject) => {
|
|
47
|
-
const parsed = new URL(url);
|
|
48
|
-
const payload = JSON.stringify(body);
|
|
49
|
-
const options = buildRequestOptions(parsed, payload);
|
|
50
|
-
const transport = selectTransport(parsed.protocol);
|
|
51
|
-
|
|
52
|
-
const req = transport.request(options, (res) => {
|
|
53
|
-
let data = '';
|
|
54
|
-
res.setEncoding('utf8');
|
|
55
|
-
res.on('data', (chunk: string) => { data += chunk; });
|
|
56
|
-
res.on('end', () => {
|
|
57
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
58
|
-
resolve(data);
|
|
59
|
-
} else {
|
|
60
|
-
reject(new Error(
|
|
61
|
-
`HTTP request failed with status ${res.statusCode}: ${data}`
|
|
62
|
-
));
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
req.on('error', (e) => {
|
|
68
|
-
reject(new Error(`HTTP request error: ${e.message}`));
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
req.write(payload);
|
|
72
|
-
req.end();
|
|
73
|
-
});
|
|
74
|
-
}
|