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,294 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { Classifier } from '../../../src/ai/classifier.js';
|
|
3
|
+
|
|
4
|
+
describe('Unit: Classifier', () => {
|
|
5
|
+
let classifier;
|
|
6
|
+
let mockLLM;
|
|
7
|
+
let mockHolosphere;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
|
|
12
|
+
mockLLM = {
|
|
13
|
+
getJSON: vi.fn()
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
mockHolosphere = {
|
|
17
|
+
put: vi.fn().mockResolvedValue({ success: true })
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
classifier = new Classifier(mockLLM, mockHolosphere);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Constructor', () => {
|
|
24
|
+
it('should initialize with LLM service', () => {
|
|
25
|
+
const c = new Classifier(mockLLM);
|
|
26
|
+
expect(c.llm).toBe(mockLLM);
|
|
27
|
+
expect(c.holosphere).toBeNull();
|
|
28
|
+
expect(c.lensDescriptions).toBeInstanceOf(Map);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should accept optional HoloSphere instance', () => {
|
|
32
|
+
expect(classifier.holosphere).toBe(mockHolosphere);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('setHoloSphere', () => {
|
|
37
|
+
it('should set HoloSphere instance', () => {
|
|
38
|
+
const c = new Classifier(mockLLM);
|
|
39
|
+
c.setHoloSphere(mockHolosphere);
|
|
40
|
+
expect(c.holosphere).toBe(mockHolosphere);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('registerLens', () => {
|
|
45
|
+
it('should register a lens with description', () => {
|
|
46
|
+
classifier.registerLens('projects', 'Contains project data');
|
|
47
|
+
|
|
48
|
+
expect(classifier.lensDescriptions.has('projects')).toBe(true);
|
|
49
|
+
expect(classifier.lensDescriptions.get('projects').description).toBe('Contains project data');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should register a lens with schema', () => {
|
|
53
|
+
const schema = { type: 'object', properties: { name: { type: 'string' } } };
|
|
54
|
+
classifier.registerLens('tasks', 'Task items', schema);
|
|
55
|
+
|
|
56
|
+
expect(classifier.lensDescriptions.get('tasks').schema).toEqual(schema);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('registerLenses', () => {
|
|
61
|
+
it('should register multiple lenses', () => {
|
|
62
|
+
classifier.registerLenses({
|
|
63
|
+
projects: { description: 'Project data' },
|
|
64
|
+
tasks: { description: 'Task data', schema: { type: 'object' } }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(classifier.lensDescriptions.size).toBe(2);
|
|
68
|
+
expect(classifier.lensDescriptions.has('projects')).toBe(true);
|
|
69
|
+
expect(classifier.lensDescriptions.has('tasks')).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('classifyToLens', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
classifier.registerLenses({
|
|
76
|
+
projects: { description: 'Project and initiative data' },
|
|
77
|
+
events: { description: 'Events and meetings' },
|
|
78
|
+
resources: { description: 'Resources and materials' }
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should classify content to best lens', async () => {
|
|
83
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
84
|
+
lens: 'projects',
|
|
85
|
+
confidence: 0.9,
|
|
86
|
+
reasoning: 'Content describes a project'
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await classifier.classifyToLens({ title: 'New Project', description: 'A software project' });
|
|
90
|
+
|
|
91
|
+
expect(result.lens).toBe('projects');
|
|
92
|
+
expect(result.confidence).toBe(0.9);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should throw error if no lenses registered', async () => {
|
|
96
|
+
const c = new Classifier(mockLLM);
|
|
97
|
+
|
|
98
|
+
await expect(c.classifyToLens('content'))
|
|
99
|
+
.rejects.toThrow('No lenses registered');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle string content', async () => {
|
|
103
|
+
mockLLM.getJSON.mockResolvedValue({ lens: 'events', confidence: 0.8, reasoning: 'Event description' });
|
|
104
|
+
|
|
105
|
+
await classifier.classifyToLens('Meeting tomorrow at 3pm');
|
|
106
|
+
|
|
107
|
+
const call = mockLLM.getJSON.mock.calls[0];
|
|
108
|
+
expect(call[1]).toBe('Meeting tomorrow at 3pm');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle object content', async () => {
|
|
112
|
+
mockLLM.getJSON.mockResolvedValue({ lens: 'resources', confidence: 0.7, reasoning: 'Resource item' });
|
|
113
|
+
|
|
114
|
+
const content = { type: 'document', name: 'Guide.pdf' };
|
|
115
|
+
await classifier.classifyToLens(content);
|
|
116
|
+
|
|
117
|
+
const call = mockLLM.getJSON.mock.calls[0];
|
|
118
|
+
expect(call[1]).toContain('Guide.pdf');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('classifyMultiple', () => {
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
classifier.registerLenses({
|
|
125
|
+
projects: { description: 'Projects' },
|
|
126
|
+
events: { description: 'Events' },
|
|
127
|
+
resources: { description: 'Resources' }
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return multiple lens classifications', async () => {
|
|
132
|
+
mockLLM.getJSON.mockResolvedValue([
|
|
133
|
+
{ lens: 'projects', confidence: 0.8 },
|
|
134
|
+
{ lens: 'resources', confidence: 0.6 }
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const result = await classifier.classifyMultiple('Multi-purpose content', 3);
|
|
138
|
+
|
|
139
|
+
expect(Array.isArray(result)).toBe(true);
|
|
140
|
+
expect(result[0].lens).toBe('projects');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should throw error if no lenses registered', async () => {
|
|
144
|
+
const c = new Classifier(mockLLM);
|
|
145
|
+
|
|
146
|
+
await expect(c.classifyMultiple('content'))
|
|
147
|
+
.rejects.toThrow('No lenses registered');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('autoStore', () => {
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
classifier.registerLens('projects', 'Project data');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should classify and store content', async () => {
|
|
157
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
158
|
+
lens: 'projects',
|
|
159
|
+
confidence: 0.9,
|
|
160
|
+
reasoning: 'Project content'
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const content = { title: 'My Project' };
|
|
164
|
+
const result = await classifier.autoStore('holon1', content);
|
|
165
|
+
|
|
166
|
+
expect(result.lens).toBe('projects');
|
|
167
|
+
expect(result.stored).toBe(true);
|
|
168
|
+
expect(mockHolosphere.put).toHaveBeenCalledWith('holon1', 'projects', content);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should throw error if HoloSphere not available', async () => {
|
|
172
|
+
const c = new Classifier(mockLLM);
|
|
173
|
+
c.registerLens('test', 'Test');
|
|
174
|
+
|
|
175
|
+
await expect(c.autoStore('holon', { data: 'test' }))
|
|
176
|
+
.rejects.toThrow('HoloSphere instance required for storage');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('suggestNewLens', () => {
|
|
181
|
+
it('should suggest a new lens for content type', async () => {
|
|
182
|
+
classifier.registerLens('projects', 'Projects');
|
|
183
|
+
|
|
184
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
185
|
+
name: 'recipes',
|
|
186
|
+
description: 'Cooking recipes and food preparation guides'
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const result = await classifier.suggestNewLens('Cooking recipes with ingredients and steps');
|
|
190
|
+
|
|
191
|
+
expect(result.name).toBe('recipes');
|
|
192
|
+
expect(result.description).toContain('recipes');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should include existing lenses in prompt', async () => {
|
|
196
|
+
classifier.registerLenses({
|
|
197
|
+
projects: { description: 'Projects' },
|
|
198
|
+
tasks: { description: 'Tasks' }
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
mockLLM.getJSON.mockResolvedValue({ name: 'new_lens', description: 'New lens' });
|
|
202
|
+
|
|
203
|
+
await classifier.suggestNewLens('New content type');
|
|
204
|
+
|
|
205
|
+
const call = mockLLM.getJSON.mock.calls[0];
|
|
206
|
+
expect(call[0]).toContain('projects');
|
|
207
|
+
expect(call[0]).toContain('tasks');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('suggestSchema', () => {
|
|
212
|
+
it('should suggest JSON schema for content', async () => {
|
|
213
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
title: { type: 'string', description: 'Item title' },
|
|
217
|
+
priority: { type: 'number', description: 'Priority level' }
|
|
218
|
+
},
|
|
219
|
+
required: ['title']
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const content = { title: 'Sample Task', priority: 1 };
|
|
223
|
+
const result = await classifier.suggestSchema(content);
|
|
224
|
+
|
|
225
|
+
expect(result.type).toBe('object');
|
|
226
|
+
expect(result.properties.title).toBeDefined();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('validateForLens', () => {
|
|
231
|
+
it('should throw error if lens not found', async () => {
|
|
232
|
+
await expect(classifier.validateForLens({ data: 'test' }, 'nonexistent'))
|
|
233
|
+
.rejects.toThrow('Lens not found: nonexistent');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should use AI validation if no schema', async () => {
|
|
237
|
+
classifier.registerLens('projects', 'Project data');
|
|
238
|
+
|
|
239
|
+
mockLLM.getJSON.mockResolvedValue({
|
|
240
|
+
valid: true,
|
|
241
|
+
issues: []
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const result = await classifier.validateForLens({ title: 'Project' }, 'projects');
|
|
245
|
+
|
|
246
|
+
expect(result.valid).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should return valid if schema exists', async () => {
|
|
250
|
+
classifier.registerLens('tasks', 'Tasks', { type: 'object' });
|
|
251
|
+
|
|
252
|
+
const result = await classifier.validateForLens({ data: 'test' }, 'tasks');
|
|
253
|
+
|
|
254
|
+
expect(result.valid).toBe(true);
|
|
255
|
+
expect(result.issues).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('analyzeCollection', () => {
|
|
260
|
+
beforeEach(() => {
|
|
261
|
+
classifier.registerLenses({
|
|
262
|
+
projects: { description: 'Projects' },
|
|
263
|
+
tasks: { description: 'Tasks' }
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should analyze and classify collection of items', async () => {
|
|
268
|
+
mockLLM.getJSON
|
|
269
|
+
.mockResolvedValueOnce({ lens: 'projects', confidence: 0.9, reasoning: 'Project' })
|
|
270
|
+
.mockResolvedValueOnce({ lens: 'tasks', confidence: 0.8, reasoning: 'Task' })
|
|
271
|
+
.mockResolvedValueOnce({ lens: 'projects', confidence: 0.7, reasoning: 'Project' });
|
|
272
|
+
|
|
273
|
+
const items = [
|
|
274
|
+
{ title: 'Project 1' },
|
|
275
|
+
{ title: 'Task 1' },
|
|
276
|
+
{ title: 'Project 2' }
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
const result = await classifier.analyzeCollection(items);
|
|
280
|
+
|
|
281
|
+
expect(result.total).toBe(3);
|
|
282
|
+
expect(result.byLens.projects).toBe(2);
|
|
283
|
+
expect(result.byLens.tasks).toBe(1);
|
|
284
|
+
expect(result.classifications).toHaveLength(3);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should handle empty collection', async () => {
|
|
288
|
+
const result = await classifier.analyzeCollection([]);
|
|
289
|
+
|
|
290
|
+
expect(result.total).toBe(0);
|
|
291
|
+
expect(result.classifications).toHaveLength(0);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { Council } from '../../../src/ai/council.js';
|
|
3
|
+
|
|
4
|
+
describe('Unit: Council', () => {
|
|
5
|
+
let council;
|
|
6
|
+
let mockLLM;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
vi.clearAllMocks();
|
|
10
|
+
|
|
11
|
+
mockLLM = {
|
|
12
|
+
sendMessage: vi.fn()
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
council = new Council(mockLLM);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('Constructor', () => {
|
|
19
|
+
it('should initialize with LLM service', () => {
|
|
20
|
+
expect(council.llm).toBe(mockLLM);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should use default perspectives', () => {
|
|
24
|
+
expect(council.perspectives).toHaveLength(12);
|
|
25
|
+
expect(council.perspectives[0].name).toBe('Values & Worldview');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should accept custom perspectives', () => {
|
|
29
|
+
const customPerspectives = [
|
|
30
|
+
{ name: 'Tech', prompt: 'From tech perspective' },
|
|
31
|
+
{ name: 'Design', prompt: 'From design perspective' }
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const c = new Council(mockLLM, customPerspectives);
|
|
35
|
+
|
|
36
|
+
expect(c.perspectives).toHaveLength(2);
|
|
37
|
+
expect(c.perspectives[0].name).toBe('Tech');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('setPerspectives', () => {
|
|
42
|
+
it('should set custom perspectives', () => {
|
|
43
|
+
const newPerspectives = [
|
|
44
|
+
{ name: 'Custom1', prompt: 'Prompt 1' }
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
council.setPerspectives(newPerspectives);
|
|
48
|
+
|
|
49
|
+
expect(council.perspectives).toHaveLength(1);
|
|
50
|
+
expect(council.perspectives[0].name).toBe('Custom1');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('getDefaultPerspectives', () => {
|
|
55
|
+
it('should return default 12 perspectives', () => {
|
|
56
|
+
const defaults = Council.getDefaultPerspectives();
|
|
57
|
+
|
|
58
|
+
expect(defaults).toHaveLength(12);
|
|
59
|
+
expect(defaults.map(p => p.name)).toContain('Health & Wellbeing');
|
|
60
|
+
expect(defaults.map(p => p.name)).toContain('Climate & Environment');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('ask', () => {
|
|
65
|
+
it('should get answers from all perspectives in parallel', async () => {
|
|
66
|
+
mockLLM.sendMessage.mockResolvedValue('Perspective answer');
|
|
67
|
+
|
|
68
|
+
const result = await council.ask('What is the meaning of life?');
|
|
69
|
+
|
|
70
|
+
expect(result.question).toBe('What is the meaning of life?');
|
|
71
|
+
expect(result.perspectives).toHaveLength(12);
|
|
72
|
+
expect(result.perspectives[0].perspective).toBe('Values & Worldview');
|
|
73
|
+
expect(result.perspectives[0].answer).toBe('Perspective answer');
|
|
74
|
+
expect(result.summary).toBeDefined();
|
|
75
|
+
expect(result.timestamp).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should get answers sequentially when parallel is false', async () => {
|
|
79
|
+
mockLLM.sendMessage.mockResolvedValue('Answer');
|
|
80
|
+
|
|
81
|
+
const result = await council.ask('Question', { parallel: false });
|
|
82
|
+
|
|
83
|
+
expect(result.perspectives).toHaveLength(12);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should skip summary when includeSummary is false', async () => {
|
|
87
|
+
mockLLM.sendMessage.mockResolvedValue('Answer');
|
|
88
|
+
|
|
89
|
+
const result = await council.ask('Question', { includeSummary: false });
|
|
90
|
+
|
|
91
|
+
expect(result.summary).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should use custom perspectives when provided', async () => {
|
|
95
|
+
mockLLM.sendMessage.mockResolvedValue('Answer');
|
|
96
|
+
|
|
97
|
+
const customPerspectives = [
|
|
98
|
+
{ name: 'Expert', prompt: 'As an expert' }
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const result = await council.ask('Question', { perspectives: customPerspectives });
|
|
102
|
+
|
|
103
|
+
expect(result.perspectives).toHaveLength(1);
|
|
104
|
+
expect(result.perspectives[0].perspective).toBe('Expert');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should generate summary across perspectives', async () => {
|
|
108
|
+
mockLLM.sendMessage
|
|
109
|
+
.mockResolvedValueOnce('Answer 1') // First perspective
|
|
110
|
+
.mockResolvedValueOnce('Answer 2') // Second perspective (and so on...)
|
|
111
|
+
.mockResolvedValue('Answer');
|
|
112
|
+
|
|
113
|
+
council.setPerspectives([
|
|
114
|
+
{ name: 'P1', prompt: 'Prompt 1' },
|
|
115
|
+
{ name: 'P2', prompt: 'Prompt 2' }
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
await council.ask('Question');
|
|
119
|
+
|
|
120
|
+
// Last call should be the summary
|
|
121
|
+
const lastCall = mockLLM.sendMessage.mock.calls[mockLLM.sendMessage.mock.calls.length - 1];
|
|
122
|
+
expect(lastCall[0]).toContain('Synthesize');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('_askPerspective', () => {
|
|
127
|
+
it('should send message with perspective prompt', async () => {
|
|
128
|
+
mockLLM.sendMessage.mockResolvedValue('Response');
|
|
129
|
+
|
|
130
|
+
const perspective = { name: 'Test', prompt: 'From test view' };
|
|
131
|
+
await council._askPerspective('Question?', perspective);
|
|
132
|
+
|
|
133
|
+
expect(mockLLM.sendMessage).toHaveBeenCalledWith(
|
|
134
|
+
expect.stringContaining('From test view'),
|
|
135
|
+
'Question?',
|
|
136
|
+
expect.objectContaining({ temperature: 0.7 })
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('_summarize', () => {
|
|
142
|
+
it('should synthesize multiple perspective answers', async () => {
|
|
143
|
+
mockLLM.sendMessage.mockResolvedValue('Synthesized summary');
|
|
144
|
+
|
|
145
|
+
const answers = [
|
|
146
|
+
{ perspective: 'P1', answer: 'Answer 1' },
|
|
147
|
+
{ perspective: 'P2', answer: 'Answer 2' }
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const result = await council._summarize('Question', answers);
|
|
151
|
+
|
|
152
|
+
expect(result).toBe('Synthesized summary');
|
|
153
|
+
expect(mockLLM.sendMessage).toHaveBeenCalledWith(
|
|
154
|
+
expect.stringContaining('wise facilitator'),
|
|
155
|
+
expect.stringContaining('Answer 1'),
|
|
156
|
+
expect.objectContaining({ temperature: 0.5 })
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('createPerspectives', () => {
|
|
162
|
+
it('should create perspectives from names', () => {
|
|
163
|
+
const names = ['Technology', 'Economics', 'Society'];
|
|
164
|
+
const perspectives = Council.createPerspectives(names);
|
|
165
|
+
|
|
166
|
+
expect(perspectives).toHaveLength(3);
|
|
167
|
+
expect(perspectives[0].name).toBe('Technology');
|
|
168
|
+
expect(perspectives[0].prompt).toContain('Technology');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('askCustom', () => {
|
|
173
|
+
it('should ask with custom perspective names', async () => {
|
|
174
|
+
mockLLM.sendMessage.mockResolvedValue('Answer');
|
|
175
|
+
|
|
176
|
+
const result = await council.askCustom('Question', ['Tech', 'Ethics']);
|
|
177
|
+
|
|
178
|
+
expect(result.perspectives).toHaveLength(2);
|
|
179
|
+
expect(result.perspectives[0].perspective).toBe('Tech');
|
|
180
|
+
expect(result.perspectives[1].perspective).toBe('Ethics');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('askSingle', () => {
|
|
185
|
+
it('should get answer from single named perspective', async () => {
|
|
186
|
+
mockLLM.sendMessage.mockResolvedValue('Single answer');
|
|
187
|
+
|
|
188
|
+
const result = await council.askSingle('Question', 'Health & Wellbeing');
|
|
189
|
+
|
|
190
|
+
expect(result).toBe('Single answer');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should create perspective if not found', async () => {
|
|
194
|
+
mockLLM.sendMessage.mockResolvedValue('Custom perspective answer');
|
|
195
|
+
|
|
196
|
+
const result = await council.askSingle('Question', 'Custom View');
|
|
197
|
+
|
|
198
|
+
expect(result).toBe('Custom perspective answer');
|
|
199
|
+
expect(mockLLM.sendMessage).toHaveBeenCalledWith(
|
|
200
|
+
expect.stringContaining('Custom View'),
|
|
201
|
+
expect.any(String),
|
|
202
|
+
expect.any(Object)
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('debate', () => {
|
|
208
|
+
it('should conduct debate between two perspectives', async () => {
|
|
209
|
+
mockLLM.sendMessage.mockResolvedValue('Debate response');
|
|
210
|
+
|
|
211
|
+
const result = await council.debate('Climate policy', ['Climate & Environment', 'Economy & Wealth'], 2);
|
|
212
|
+
|
|
213
|
+
expect(result.topic).toBe('Climate policy');
|
|
214
|
+
expect(result.perspectives).toEqual(['Climate & Environment', 'Economy & Wealth']);
|
|
215
|
+
expect(result.exchanges).toHaveLength(4); // 2 rounds * 2 perspectives
|
|
216
|
+
expect(result.conclusion).toBeDefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should throw error if not exactly 2 perspectives', async () => {
|
|
220
|
+
await expect(council.debate('Topic', ['One']))
|
|
221
|
+
.rejects.toThrow('Debate requires exactly 2 perspectives');
|
|
222
|
+
|
|
223
|
+
await expect(council.debate('Topic', ['One', 'Two', 'Three']))
|
|
224
|
+
.rejects.toThrow('Debate requires exactly 2 perspectives');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should build context through debate rounds', async () => {
|
|
228
|
+
let callCount = 0;
|
|
229
|
+
mockLLM.sendMessage.mockImplementation(() => {
|
|
230
|
+
callCount++;
|
|
231
|
+
return Promise.resolve(`Response ${callCount}`);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await council.debate('Topic', ['A', 'B'], 2);
|
|
235
|
+
|
|
236
|
+
// Later calls should include previous responses in context
|
|
237
|
+
const laterCalls = mockLLM.sendMessage.mock.calls.slice(2);
|
|
238
|
+
laterCalls.forEach(call => {
|
|
239
|
+
expect(call[1]).toContain('Response');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should generate conclusion after debate', async () => {
|
|
244
|
+
mockLLM.sendMessage.mockResolvedValue('Response');
|
|
245
|
+
|
|
246
|
+
const result = await council.debate('Topic', ['A', 'B'], 1);
|
|
247
|
+
|
|
248
|
+
// Last call should be conclusion
|
|
249
|
+
const lastCall = mockLLM.sendMessage.mock.calls[mockLLM.sendMessage.mock.calls.length - 1];
|
|
250
|
+
expect(lastCall[0]).toContain('moderator');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should use existing perspective prompts when available', async () => {
|
|
254
|
+
mockLLM.sendMessage.mockResolvedValue('Response');
|
|
255
|
+
|
|
256
|
+
await council.debate('Topic', ['Health & Wellbeing', 'Climate & Environment'], 1);
|
|
257
|
+
|
|
258
|
+
const calls = mockLLM.sendMessage.mock.calls;
|
|
259
|
+
expect(calls[0][0]).toContain('Health');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|