holosphere 1.1.20 → 2.0.0-alpha1
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/.env.example +36 -0
- package/.eslintrc.json +16 -0
- package/.prettierrc.json +7 -0
- package/LICENSE +162 -38
- package/README.md +483 -367
- package/bin/holosphere-activitypub.js +158 -0
- package/cleanup-test-data.js +204 -0
- package/examples/demo.html +1333 -0
- package/examples/example-bot.js +197 -0
- package/package.json +47 -87
- package/scripts/check-bundle-size.js +54 -0
- package/scripts/check-quest-ids.js +77 -0
- package/scripts/import-holons.js +578 -0
- package/scripts/publish-to-relay.js +101 -0
- package/scripts/read-example.js +186 -0
- package/scripts/relay-diagnostic.js +59 -0
- package/scripts/relay-example.js +179 -0
- package/scripts/resync-to-relay.js +245 -0
- package/scripts/revert-import.js +196 -0
- package/scripts/test-hybrid-mode.js +108 -0
- package/scripts/test-local-storage.js +63 -0
- package/scripts/test-nostr-direct.js +55 -0
- package/scripts/test-read-data.js +45 -0
- package/scripts/test-write-read.js +63 -0
- package/scripts/verify-import.js +95 -0
- package/scripts/verify-relay-data.js +139 -0
- package/src/ai/aggregation.js +319 -0
- package/src/ai/breakdown.js +511 -0
- package/src/ai/classifier.js +217 -0
- package/src/ai/council.js +228 -0
- package/src/ai/embeddings.js +279 -0
- package/src/ai/federation-ai.js +324 -0
- package/src/ai/h3-ai.js +955 -0
- package/src/ai/index.js +112 -0
- package/src/ai/json-ops.js +225 -0
- package/src/ai/llm-service.js +205 -0
- package/src/ai/nl-query.js +223 -0
- package/src/ai/relationships.js +353 -0
- package/src/ai/schema-extractor.js +218 -0
- package/src/ai/spatial.js +293 -0
- package/src/ai/tts.js +194 -0
- package/src/content/social-protocols.js +168 -0
- package/src/core/holosphere.js +273 -0
- package/src/crypto/secp256k1.js +259 -0
- package/src/federation/discovery.js +334 -0
- package/src/federation/hologram.js +1042 -0
- package/src/federation/registry.js +386 -0
- package/src/hierarchical/upcast.js +110 -0
- package/src/index.js +2669 -0
- package/src/schema/validator.js +91 -0
- package/src/spatial/h3-operations.js +110 -0
- package/src/storage/backend-factory.js +125 -0
- package/src/storage/backend-interface.js +142 -0
- package/src/storage/backends/activitypub/server.js +653 -0
- package/src/storage/backends/activitypub-backend.js +272 -0
- package/src/storage/backends/gundb-backend.js +233 -0
- package/src/storage/backends/nostr-backend.js +136 -0
- package/src/storage/filesystem-storage-browser.js +41 -0
- package/src/storage/filesystem-storage.js +138 -0
- package/src/storage/global-tables.js +81 -0
- package/src/storage/gun-async.js +281 -0
- package/src/storage/gun-wrapper.js +221 -0
- package/src/storage/indexeddb-storage.js +122 -0
- package/src/storage/key-storage-simple.js +76 -0
- package/src/storage/key-storage.js +136 -0
- package/src/storage/memory-storage.js +59 -0
- package/src/storage/migration.js +338 -0
- package/src/storage/nostr-async.js +811 -0
- package/src/storage/nostr-client.js +939 -0
- package/src/storage/nostr-wrapper.js +211 -0
- package/src/storage/outbox-queue.js +208 -0
- package/src/storage/persistent-storage.js +109 -0
- package/src/storage/sync-service.js +164 -0
- package/src/subscriptions/manager.js +142 -0
- package/test-ai-real-api.js +202 -0
- package/tests/unit/ai/aggregation.test.js +295 -0
- package/tests/unit/ai/breakdown.test.js +446 -0
- package/tests/unit/ai/classifier.test.js +294 -0
- package/tests/unit/ai/council.test.js +262 -0
- package/tests/unit/ai/embeddings.test.js +384 -0
- package/tests/unit/ai/federation-ai.test.js +344 -0
- package/tests/unit/ai/h3-ai.test.js +458 -0
- package/tests/unit/ai/index.test.js +304 -0
- package/tests/unit/ai/json-ops.test.js +307 -0
- package/tests/unit/ai/llm-service.test.js +390 -0
- package/tests/unit/ai/nl-query.test.js +383 -0
- package/tests/unit/ai/relationships.test.js +311 -0
- package/tests/unit/ai/schema-extractor.test.js +384 -0
- package/tests/unit/ai/spatial.test.js +279 -0
- package/tests/unit/ai/tts.test.js +279 -0
- package/tests/unit/content.test.js +332 -0
- package/tests/unit/contract/core.test.js +88 -0
- package/tests/unit/contract/crypto.test.js +198 -0
- package/tests/unit/contract/data.test.js +223 -0
- package/tests/unit/contract/federation.test.js +181 -0
- package/tests/unit/contract/hierarchical.test.js +113 -0
- package/tests/unit/contract/schema.test.js +114 -0
- package/tests/unit/contract/social.test.js +217 -0
- package/tests/unit/contract/spatial.test.js +110 -0
- package/tests/unit/contract/subscriptions.test.js +128 -0
- package/tests/unit/contract/utils.test.js +159 -0
- package/tests/unit/core.test.js +152 -0
- package/tests/unit/crypto.test.js +328 -0
- package/tests/unit/federation.test.js +234 -0
- package/tests/unit/gun-async.test.js +252 -0
- package/tests/unit/hierarchical.test.js +399 -0
- package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
- package/tests/unit/integration/scenario-02-federation.test.js +76 -0
- package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
- package/tests/unit/integration/scenario-04-validation.test.js +129 -0
- package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
- package/tests/unit/integration/scenario-06-social.test.js +135 -0
- package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
- package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
- package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
- package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
- package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
- package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
- package/tests/unit/performance/benchmark.test.js +85 -0
- package/tests/unit/schema.test.js +213 -0
- package/tests/unit/spatial.test.js +158 -0
- package/tests/unit/storage.test.js +195 -0
- package/tests/unit/subscriptions.test.js +328 -0
- package/tests/unit/test-data-permanence-debug.js +197 -0
- package/tests/unit/test-data-permanence.js +340 -0
- package/tests/unit/test-key-persistence-fixed.js +148 -0
- package/tests/unit/test-key-persistence.js +172 -0
- package/tests/unit/test-relay-permanence.js +376 -0
- package/tests/unit/test-second-node.js +95 -0
- package/tests/unit/test-simple-write.js +89 -0
- package/vite.config.js +49 -0
- package/vitest.config.js +20 -0
- package/FEDERATION.md +0 -213
- package/compute.js +0 -298
- package/content.js +0 -980
- package/federation.js +0 -1234
- package/global.js +0 -736
- package/hexlib.js +0 -335
- package/hologram.js +0 -183
- package/holosphere-bundle.esm.js +0 -33256
- package/holosphere-bundle.js +0 -33287
- package/holosphere-bundle.min.js +0 -39
- package/holosphere.d.ts +0 -601
- package/holosphere.js +0 -719
- package/node.js +0 -246
- package/schema.js +0 -139
- package/utils.js +0 -302
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-time Subscription Management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { subscribe } from '../storage/nostr-wrapper.js';
|
|
6
|
+
import { resolveHologram } from '../federation/hologram.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create subscription with throttle and filter
|
|
10
|
+
* @param {Object} client - Nostr client instance
|
|
11
|
+
* @param {string} path - Path to subscribe to
|
|
12
|
+
* @param {Function} callback - Callback function
|
|
13
|
+
* @param {Object} options - Subscription options
|
|
14
|
+
* @param {number} options.throttle - Throttle interval in ms
|
|
15
|
+
* @param {Function} options.filter - Filter predicate
|
|
16
|
+
* @param {boolean} options.includeFederated - Include federated data
|
|
17
|
+
* @param {boolean} options.triggerInitial - Trigger callback with initial data (default: false)
|
|
18
|
+
* @param {boolean} options.resolveHolograms - Resolve holograms before callback (default: true)
|
|
19
|
+
* @returns {Object} Subscription object with unsubscribe method
|
|
20
|
+
*/
|
|
21
|
+
export async function createSubscription(client, path, callback, options = {}) {
|
|
22
|
+
const { throttle = 0, filter = null, includeFederated = false, triggerInitial = false, realtimeOnly = true, resolveHolograms = true } = options;
|
|
23
|
+
|
|
24
|
+
let lastInvoke = 0;
|
|
25
|
+
let timeoutId = null;
|
|
26
|
+
const seenKeys = new Set();
|
|
27
|
+
let isActive = true; // Track active state
|
|
28
|
+
|
|
29
|
+
const wrappedCallback = async (data, key) => {
|
|
30
|
+
// Check if subscription is still active
|
|
31
|
+
if (!isActive) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Skip if already seen (prevent duplicates)
|
|
36
|
+
// Include timestamp in unique key to allow updates to same item
|
|
37
|
+
const uniqueKey = `${key}-${data?.id || ''}-${data?._meta?.timestamp || Date.now()}`;
|
|
38
|
+
if (seenKeys.has(uniqueKey)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
seenKeys.add(uniqueKey);
|
|
42
|
+
|
|
43
|
+
// Resolve holograms if enabled and data is a hologram
|
|
44
|
+
let resolvedData = data;
|
|
45
|
+
if (resolveHolograms && data && data.hologram === true) {
|
|
46
|
+
try {
|
|
47
|
+
resolvedData = await resolveHologram(client, data);
|
|
48
|
+
// If resolution failed, skip this item (source data may not exist yet)
|
|
49
|
+
if (!resolvedData) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn('Failed to resolve hologram in subscription:', err);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Apply filter (on resolved data)
|
|
59
|
+
if (filter && !filter(resolvedData, key)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Apply throttle
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (throttle > 0) {
|
|
66
|
+
if (now - lastInvoke < throttle) {
|
|
67
|
+
// Clear existing timeout
|
|
68
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
69
|
+
|
|
70
|
+
// Schedule callback
|
|
71
|
+
timeoutId = setTimeout(() => {
|
|
72
|
+
lastInvoke = Date.now();
|
|
73
|
+
callback(resolvedData, key);
|
|
74
|
+
}, throttle - (now - lastInvoke));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lastInvoke = now;
|
|
80
|
+
callback(resolvedData, key);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Subscribe using Nostr wrapper
|
|
84
|
+
const subscription = await subscribe(client, path, wrappedCallback, { realtimeOnly });
|
|
85
|
+
|
|
86
|
+
// Return subscription object
|
|
87
|
+
return {
|
|
88
|
+
path,
|
|
89
|
+
unsubscribe: () => {
|
|
90
|
+
isActive = false; // Mark as inactive immediately
|
|
91
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
92
|
+
if (subscription && subscription.unsubscribe) {
|
|
93
|
+
subscription.unsubscribe();
|
|
94
|
+
}
|
|
95
|
+
seenKeys.clear();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Subscription registry for tracking active subscriptions
|
|
102
|
+
*/
|
|
103
|
+
export class SubscriptionRegistry {
|
|
104
|
+
constructor() {
|
|
105
|
+
this.subscriptions = new Map();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Register a subscription
|
|
110
|
+
*/
|
|
111
|
+
register(id, subscription) {
|
|
112
|
+
this.subscriptions.set(id, subscription);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Unregister a subscription
|
|
117
|
+
*/
|
|
118
|
+
unregister(id) {
|
|
119
|
+
const subscription = this.subscriptions.get(id);
|
|
120
|
+
if (subscription) {
|
|
121
|
+
subscription.unsubscribe();
|
|
122
|
+
this.subscriptions.delete(id);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Unsubscribe all
|
|
128
|
+
*/
|
|
129
|
+
unsubscribeAll() {
|
|
130
|
+
for (const [id, subscription] of this.subscriptions) {
|
|
131
|
+
subscription.unsubscribe();
|
|
132
|
+
}
|
|
133
|
+
this.subscriptions.clear();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get active subscription count
|
|
138
|
+
*/
|
|
139
|
+
count() {
|
|
140
|
+
return this.subscriptions.size;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test AI Module with Real OpenAI API Calls
|
|
3
|
+
*
|
|
4
|
+
* Run with: node test-ai-real-api.js
|
|
5
|
+
*
|
|
6
|
+
* Requires OPENAI_API_KEY environment variable to be set
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import 'dotenv/config';
|
|
10
|
+
import { LLMService } from './src/ai/llm-service.js';
|
|
11
|
+
import { Embeddings } from './src/ai/embeddings.js';
|
|
12
|
+
import { SchemaExtractor } from './src/ai/schema-extractor.js';
|
|
13
|
+
import { Classifier } from './src/ai/classifier.js';
|
|
14
|
+
import { NLQuery } from './src/ai/nl-query.js';
|
|
15
|
+
import { createAIServices } from './src/ai/index.js';
|
|
16
|
+
import OpenAI from 'openai';
|
|
17
|
+
|
|
18
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
19
|
+
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
console.error('ERROR: OPENAI_API_KEY environment variable is not set');
|
|
22
|
+
console.error('Please set it in your .env file or export it:');
|
|
23
|
+
console.error(' export OPENAI_API_KEY=sk-...');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('='.repeat(60));
|
|
28
|
+
console.log(' HoloSphere AI Module - Real API Test');
|
|
29
|
+
console.log('='.repeat(60));
|
|
30
|
+
console.log(`API Key: ${apiKey.substring(0, 10)}...${apiKey.substring(apiKey.length - 4)}`);
|
|
31
|
+
console.log('');
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
|
|
36
|
+
async function test(name, fn) {
|
|
37
|
+
process.stdout.write(`Testing ${name}... `);
|
|
38
|
+
try {
|
|
39
|
+
const result = await fn();
|
|
40
|
+
console.log('PASSED');
|
|
41
|
+
if (result) console.log(` Result: ${JSON.stringify(result).substring(0, 100)}...`);
|
|
42
|
+
passed++;
|
|
43
|
+
return result;
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.log('FAILED');
|
|
46
|
+
console.log(` Error: ${err.message}`);
|
|
47
|
+
failed++;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Initialize services
|
|
53
|
+
const llm = new LLMService(apiKey);
|
|
54
|
+
const openai = new OpenAI({ apiKey });
|
|
55
|
+
const embeddings = new Embeddings(openai);
|
|
56
|
+
|
|
57
|
+
console.log('\n--- LLM Service Tests ---\n');
|
|
58
|
+
|
|
59
|
+
// Test 1: Basic chat completion
|
|
60
|
+
await test('LLM sendMessage', async () => {
|
|
61
|
+
const response = await llm.sendMessage(
|
|
62
|
+
'You are a helpful assistant. Respond in one sentence.',
|
|
63
|
+
'What is H3 hexagonal indexing?'
|
|
64
|
+
);
|
|
65
|
+
return response.substring(0, 80);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Test 2: Summarization
|
|
69
|
+
await test('LLM summarize', async () => {
|
|
70
|
+
const text = `
|
|
71
|
+
HoloSphere is a local-first, geospatially-aware database built on Nostr.
|
|
72
|
+
It uses H3 hexagonal cells to partition geographic space into holons.
|
|
73
|
+
Each holon can store data in different lenses for domain separation.
|
|
74
|
+
Federation allows holons to share data through lightweight references called holograms.
|
|
75
|
+
`;
|
|
76
|
+
const summary = await llm.summarize(text, { maxLength: 30 });
|
|
77
|
+
return summary;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Test 3: Keyword extraction
|
|
81
|
+
await test('LLM extractKeywords', async () => {
|
|
82
|
+
const keywords = await llm.extractKeywords(
|
|
83
|
+
'HoloSphere uses H3 hexagonal cells and Nostr protocol for geospatial data storage',
|
|
84
|
+
5
|
|
85
|
+
);
|
|
86
|
+
return keywords;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Test 4: JSON extraction
|
|
90
|
+
await test('LLM getJSON', async () => {
|
|
91
|
+
const json = await llm.getJSON(
|
|
92
|
+
'Extract the person info as JSON with fields: name, age, city',
|
|
93
|
+
'John Smith is 32 years old and lives in San Francisco'
|
|
94
|
+
);
|
|
95
|
+
return json;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
console.log('\n--- Embeddings Tests ---\n');
|
|
99
|
+
|
|
100
|
+
// Test 5: Single embedding
|
|
101
|
+
await test('Embeddings embed', async () => {
|
|
102
|
+
const embedding = await embeddings.embed('HoloSphere geospatial database');
|
|
103
|
+
return { dimensions: embedding.length, sample: embedding.slice(0, 3) };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Test 6: Batch embeddings
|
|
107
|
+
await test('Embeddings embedBatch', async () => {
|
|
108
|
+
const batch = await embeddings.embedBatch([
|
|
109
|
+
'Geographic data',
|
|
110
|
+
'Hexagonal cells',
|
|
111
|
+
'Federation'
|
|
112
|
+
]);
|
|
113
|
+
return { count: batch.length, dimensions: batch[0]?.length };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Test 7: Similarity calculation
|
|
117
|
+
await test('Embeddings similarity', async () => {
|
|
118
|
+
const emb1 = await embeddings.embed('San Francisco California');
|
|
119
|
+
const emb2 = await embeddings.embed('Los Angeles California');
|
|
120
|
+
const emb3 = await embeddings.embed('Programming code software');
|
|
121
|
+
|
|
122
|
+
const sim12 = embeddings.cosineSimilarity(emb1, emb2);
|
|
123
|
+
const sim13 = embeddings.cosineSimilarity(emb1, emb3);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
'SF vs LA': sim12.toFixed(3),
|
|
127
|
+
'SF vs Code': sim13.toFixed(3),
|
|
128
|
+
'Cities more similar': sim12 > sim13
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log('\n--- Schema Extractor Tests ---\n');
|
|
133
|
+
|
|
134
|
+
// Test 8: Schema extraction
|
|
135
|
+
await test('SchemaExtractor extractToSchema', async () => {
|
|
136
|
+
const extractor = new SchemaExtractor(llm);
|
|
137
|
+
const schema = {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
location: { type: 'string', description: 'Location name' },
|
|
141
|
+
latitude: { type: 'number', description: 'Latitude coordinate' },
|
|
142
|
+
longitude: { type: 'number', description: 'Longitude coordinate' }
|
|
143
|
+
},
|
|
144
|
+
required: ['location', 'latitude', 'longitude']
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const data = await extractor.extractToSchema(
|
|
148
|
+
'The Eiffel Tower is located in Paris at coordinates 48.8584 N, 2.2945 E',
|
|
149
|
+
schema
|
|
150
|
+
);
|
|
151
|
+
return data;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
console.log('\n--- Classifier Tests ---\n');
|
|
155
|
+
|
|
156
|
+
// Test 9: Content classification
|
|
157
|
+
await test('Classifier classifyToLens', async () => {
|
|
158
|
+
const classifier = new Classifier(llm);
|
|
159
|
+
classifier.registerLenses({
|
|
160
|
+
events: { description: 'Events, meetings, gatherings' },
|
|
161
|
+
projects: { description: 'Projects, initiatives, work items' },
|
|
162
|
+
resources: { description: 'Resources, documents, materials' },
|
|
163
|
+
places: { description: 'Locations, venues, geographic points' }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = await classifier.classifyToLens({
|
|
167
|
+
title: 'Community Garden Project',
|
|
168
|
+
description: 'A local initiative to create a shared vegetable garden in the park'
|
|
169
|
+
});
|
|
170
|
+
return result;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
console.log('\n--- NL Query Tests ---\n');
|
|
174
|
+
|
|
175
|
+
// Test 10: Natural language to query
|
|
176
|
+
await test('NLQuery parse', async () => {
|
|
177
|
+
const nlQuery = new NLQuery(llm);
|
|
178
|
+
const query = await nlQuery.parse(
|
|
179
|
+
'Find all coffee shops within 500 meters of downtown San Francisco'
|
|
180
|
+
);
|
|
181
|
+
return query;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log('\n--- Full Service Creation ---\n');
|
|
185
|
+
|
|
186
|
+
// Test 11: Create all services
|
|
187
|
+
await test('createAIServices', async () => {
|
|
188
|
+
const services = createAIServices(apiKey, null, {}, openai);
|
|
189
|
+
return {
|
|
190
|
+
servicesCreated: Object.keys(services).length,
|
|
191
|
+
services: Object.keys(services)
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Summary
|
|
196
|
+
console.log('\n' + '='.repeat(60));
|
|
197
|
+
console.log(` Results: ${passed} passed, ${failed} failed`);
|
|
198
|
+
console.log('='.repeat(60));
|
|
199
|
+
|
|
200
|
+
if (failed > 0) {
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { SmartAggregation } from '../../../src/ai/aggregation.js';
|
|
3
|
+
|
|
4
|
+
// Mock h3-js
|
|
5
|
+
vi.mock('h3-js', () => ({
|
|
6
|
+
getResolution: vi.fn().mockReturnValue(5),
|
|
7
|
+
cellToParent: vi.fn().mockReturnValue('parent_holon'),
|
|
8
|
+
cellToChildren: vi.fn().mockReturnValue(['child1', 'child2', 'child3'])
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('Unit: SmartAggregation', () => {
|
|
12
|
+
let aggregation;
|
|
13
|
+
let mockLLM;
|
|
14
|
+
let mockHolosphere;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
|
|
19
|
+
mockLLM = {
|
|
20
|
+
getJSON: vi.fn()
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
mockHolosphere = {
|
|
24
|
+
getAll: vi.fn().mockResolvedValue([]),
|
|
25
|
+
put: vi.fn().mockResolvedValue({ success: true })
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
aggregation = new SmartAggregation(mockLLM, mockHolosphere);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Constructor', () => {
|
|
32
|
+
it('should initialize with LLM service', () => {
|
|
33
|
+
const a = new SmartAggregation(mockLLM);
|
|
34
|
+
expect(a.llm).toBe(mockLLM);
|
|
35
|
+
expect(a.holosphere).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should accept optional HoloSphere instance', () => {
|
|
39
|
+
expect(aggregation.holosphere).toBe(mockHolosphere);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('setHoloSphere', () => {
|
|
44
|
+
it('should set HoloSphere instance', () => {
|
|
45
|
+
const a = new SmartAggregation(mockLLM);
|
|
46
|
+
a.setHoloSphere(mockHolosphere);
|
|
47
|
+
expect(a.holosphere).toBe(mockHolosphere);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('smartUpcast', () => {
|
|
52
|
+
it('should generate summary and upcast to parent', async () => {
|
|
53
|
+
mockHolosphere.getAll.mockResolvedValue([
|
|
54
|
+
{ id: 1, title: 'Item 1' },
|
|
55
|
+
{ id: 2, title: 'Item 2' }
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
59
|
+
text: 'Two items in region',
|
|
60
|
+
stats: { count: 2 },
|
|
61
|
+
notable: ['Item 1'],
|
|
62
|
+
themes: ['development']
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = await aggregation.smartUpcast('holon1', 'projects');
|
|
66
|
+
|
|
67
|
+
expect(result.holon).toBe('holon1');
|
|
68
|
+
expect(result.lens).toBe('projects');
|
|
69
|
+
expect(result.summary).toBeDefined();
|
|
70
|
+
expect(result.dataCount).toBe(2);
|
|
71
|
+
expect(result.parent).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should throw error if HoloSphere not available', async () => {
|
|
75
|
+
const a = new SmartAggregation(mockLLM);
|
|
76
|
+
|
|
77
|
+
await expect(a.smartUpcast('holon', 'lens'))
|
|
78
|
+
.rejects.toThrow('HoloSphere instance required');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should store summary at parent level when storeResults is true', async () => {
|
|
82
|
+
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }]);
|
|
83
|
+
mockLLM.getJSON.mockResolvedValue({ text: 'Summary' });
|
|
84
|
+
|
|
85
|
+
await aggregation.smartUpcast('holon1', 'projects', { storeResults: true });
|
|
86
|
+
|
|
87
|
+
expect(mockHolosphere.put).toHaveBeenCalledWith(
|
|
88
|
+
'parent_holon',
|
|
89
|
+
'projects_summaries',
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
childHolon: 'holon1',
|
|
92
|
+
summary: expect.any(Object)
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should respect maxLevels option', async () => {
|
|
98
|
+
mockHolosphere.getAll.mockResolvedValue([{ id: 1 }]);
|
|
99
|
+
mockLLM.getJSON.mockResolvedValue({ text: 'Summary' });
|
|
100
|
+
|
|
101
|
+
const result = await aggregation.smartUpcast('holon1', 'projects', { maxLevels: 1 });
|
|
102
|
+
|
|
103
|
+
// Should only go up one level
|
|
104
|
+
expect(result.parent).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('_generateSummary', () => {
|
|
109
|
+
it('should generate summary for data', async () => {
|
|
110
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
111
|
+
text: 'Active region',
|
|
112
|
+
stats: { count: 5 },
|
|
113
|
+
notable: ['Project A'],
|
|
114
|
+
themes: ['sustainability']
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const data = [{ id: 1 }, { id: 2 }];
|
|
118
|
+
const result = await aggregation._generateSummary(data, 'holon1', 'projects');
|
|
119
|
+
|
|
120
|
+
expect(result.text).toBe('Active region');
|
|
121
|
+
expect(result.stats.count).toBe(5);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return empty summary for empty data', async () => {
|
|
125
|
+
const result = await aggregation._generateSummary([], 'holon1', 'lens');
|
|
126
|
+
|
|
127
|
+
expect(result.text).toBe('No data in this region');
|
|
128
|
+
expect(result.stats.count).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('generateHolonSummary', () => {
|
|
133
|
+
it('should generate comprehensive holon summary', async () => {
|
|
134
|
+
mockHolosphere.getAll
|
|
135
|
+
.mockResolvedValueOnce([{ id: 1 }]) // projects
|
|
136
|
+
.mockResolvedValueOnce([{ id: 2 }]) // quests
|
|
137
|
+
.mockResolvedValueOnce([]) // events
|
|
138
|
+
.mockResolvedValueOnce([]) // resources
|
|
139
|
+
.mockResolvedValueOnce([{ id: 3 }]); // default
|
|
140
|
+
|
|
141
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
142
|
+
title: 'Region Summary',
|
|
143
|
+
executive_summary: 'Active region',
|
|
144
|
+
highlights: ['Growth'],
|
|
145
|
+
activities: { count: 2, summary: 'Various activities' },
|
|
146
|
+
community: { engagement_level: 'high', notes: 'Active community' },
|
|
147
|
+
resources: ['Local resources'],
|
|
148
|
+
challenges: ['Coordination'],
|
|
149
|
+
opportunities: ['Expansion'],
|
|
150
|
+
health_score: 0.8,
|
|
151
|
+
recommendations: ['Focus on coordination']
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = await aggregation.generateHolonSummary('holon1');
|
|
155
|
+
|
|
156
|
+
expect(result.holon).toBe('holon1');
|
|
157
|
+
expect(result.summary.title).toBe('Region Summary');
|
|
158
|
+
expect(result.lensesAnalyzed).toBeDefined();
|
|
159
|
+
expect(result.totalItems).toBeGreaterThan(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should throw error if HoloSphere not available', async () => {
|
|
163
|
+
const a = new SmartAggregation(mockLLM);
|
|
164
|
+
|
|
165
|
+
await expect(a.generateHolonSummary('holon'))
|
|
166
|
+
.rejects.toThrow('HoloSphere instance required');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should skip unavailable lenses', async () => {
|
|
170
|
+
mockHolosphere.getAll
|
|
171
|
+
.mockResolvedValueOnce([{ id: 1 }])
|
|
172
|
+
.mockRejectedValueOnce(new Error('Lens not found'))
|
|
173
|
+
.mockResolvedValueOnce([])
|
|
174
|
+
.mockResolvedValueOnce([])
|
|
175
|
+
.mockResolvedValueOnce([]);
|
|
176
|
+
|
|
177
|
+
mockLLM.getJSON.mockResolvedValue({ title: 'Summary' });
|
|
178
|
+
|
|
179
|
+
const result = await aggregation.generateHolonSummary('holon1');
|
|
180
|
+
|
|
181
|
+
expect(result).toBeDefined();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('aggregateChildren', () => {
|
|
186
|
+
it('should aggregate summaries from child holons', async () => {
|
|
187
|
+
mockHolosphere.getAll
|
|
188
|
+
.mockResolvedValueOnce([{ id: 1 }])
|
|
189
|
+
.mockResolvedValueOnce([{ id: 2 }])
|
|
190
|
+
.mockResolvedValueOnce([{ id: 3 }]);
|
|
191
|
+
|
|
192
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
193
|
+
text: 'Summary',
|
|
194
|
+
stats: { count: 1 }
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const result = await aggregation.aggregateChildren('parent_holon', 'projects');
|
|
198
|
+
|
|
199
|
+
expect(result.holon).toBe('parent_holon');
|
|
200
|
+
expect(result.lens).toBe('projects');
|
|
201
|
+
expect(result.childCount).toBeGreaterThan(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should throw error if HoloSphere not available', async () => {
|
|
205
|
+
const a = new SmartAggregation(mockLLM);
|
|
206
|
+
|
|
207
|
+
await expect(a.aggregateChildren('parent', 'lens'))
|
|
208
|
+
.rejects.toThrow('HoloSphere instance required');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return message when no child data found', async () => {
|
|
212
|
+
mockHolosphere.getAll.mockResolvedValue([]);
|
|
213
|
+
|
|
214
|
+
const result = await aggregation.aggregateChildren('parent_holon', 'projects');
|
|
215
|
+
|
|
216
|
+
expect(result.message).toBe('No child data found');
|
|
217
|
+
expect(result.summary).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('comparePeriods', () => {
|
|
222
|
+
it('should compare data across time periods', async () => {
|
|
223
|
+
mockHolosphere.getAll.mockResolvedValue([
|
|
224
|
+
{ id: 1, timestamp: '2024-01-15' },
|
|
225
|
+
{ id: 2, timestamp: '2024-06-15' },
|
|
226
|
+
{ id: 3, timestamp: '2024-08-15' }
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
230
|
+
growth_rate: 50,
|
|
231
|
+
direction: 'growth',
|
|
232
|
+
new_themes: ['sustainability'],
|
|
233
|
+
disappeared: ['legacy'],
|
|
234
|
+
key_changes: ['Focus shift'],
|
|
235
|
+
summary: 'Growing activity'
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result = await aggregation.comparePeriods(
|
|
239
|
+
'holon1',
|
|
240
|
+
'projects',
|
|
241
|
+
{ start: '2024-01-01', end: '2024-03-31' },
|
|
242
|
+
{ start: '2024-07-01', end: '2024-09-30' }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(result.growth_rate).toBe(50);
|
|
246
|
+
expect(result.direction).toBe('growth');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should throw error if HoloSphere not available', async () => {
|
|
250
|
+
const a = new SmartAggregation(mockLLM);
|
|
251
|
+
|
|
252
|
+
await expect(a.comparePeriods('h', 'l', {}, {}))
|
|
253
|
+
.rejects.toThrow('HoloSphere instance required');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should filter items by period', async () => {
|
|
257
|
+
mockHolosphere.getAll.mockResolvedValue([
|
|
258
|
+
{ id: 1, timestamp: '2024-01-15' },
|
|
259
|
+
{ id: 2, timestamp: '2024-02-15' },
|
|
260
|
+
{ id: 3, timestamp: '2024-06-15' }
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
mockLLM.getJSON.mockResolvedValue({ growth_rate: 0 });
|
|
264
|
+
|
|
265
|
+
await aggregation.comparePeriods(
|
|
266
|
+
'holon1',
|
|
267
|
+
'lens',
|
|
268
|
+
{ start: '2024-01-01', end: '2024-03-31' },
|
|
269
|
+
{ start: '2024-06-01', end: '2024-06-30' }
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const call = mockLLM.getJSON.mock.calls[0];
|
|
273
|
+
expect(call[1]).toContain('Period 1 data');
|
|
274
|
+
expect(call[1]).toContain('Period 2 data');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should handle items with created_at field', async () => {
|
|
278
|
+
mockHolosphere.getAll.mockResolvedValue([
|
|
279
|
+
{ id: 1, created_at: '2024-01-15' },
|
|
280
|
+
{ id: 2, created_at: '2024-06-15' }
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
mockLLM.getJSON.mockResolvedValue({ growth_rate: 100 });
|
|
284
|
+
|
|
285
|
+
await aggregation.comparePeriods(
|
|
286
|
+
'holon1',
|
|
287
|
+
'lens',
|
|
288
|
+
{ start: '2024-01-01', end: '2024-03-31' },
|
|
289
|
+
{ start: '2024-06-01', end: '2024-06-30' }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(mockLLM.getJSON).toHaveBeenCalled();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|