diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..087fe67c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,63 @@ +# Brainy Examples + +This directory contains examples demonstrating various features and use cases of the Brainy vector graph database. + +## Browser-Server Search Example + +The [browser-server-search](./browser-server-search/) example demonstrates how to use Brainy in a browser, call a server-hosted version for search, store the results locally, and then perform further searches against the local instance. + +This approach allows you to: +- Search a server-hosted Brainy instance from a browser +- Store the search results in a local Brainy instance +- Perform further searches against the local instance without needing to query the server again +- Add data to both local and server instances + +See the [browser-server-search README](./browser-server-search/README.md) for detailed instructions. + +## Other Examples + +### Augmentation Examples + +- [conduitAugmentationExample.js](./conduitAugmentationExample.js) - Demonstrates how to use conduit augmentations for syncing Brainy instances +- [memoryAugmentationExample.js](./memoryAugmentationExample.js) - Shows how to use memory augmentations for custom storage + +### Pipeline Examples + +- [sequentialPipelineExample.js](./sequentialPipelineExample.js) - Demonstrates the sequential pipeline for processing data + +### Demo + +- [demo.html](./demo.html) - A web demo showcasing Brainy's capabilities + +### Configuration Examples + +- [configurationTest.js](./configurationTest.js) - Shows how to configure Brainy with custom options +- [readOnlyTest.js](./readOnlyTest.js) - Demonstrates using Brainy in read-only mode +- [buildTimeRegistration.js](./buildTimeRegistration.js) - Shows how to register augmentations at build time + +### Data Inspection + +- [dataInspectionExample.js](./dataInspectionExample.js) - Demonstrates how to inspect data stored in Brainy + +## Running the Examples + +Most JavaScript examples can be run using Node.js: + +```bash +node examples/sequentialPipelineExample.js +``` + +For HTML examples, you can open them directly in a browser or serve them using a local HTTP server: + +```bash +# Using a simple HTTP server +npx http-server +``` + +Then navigate to the appropriate URL in your browser (e.g., http://localhost:8080/examples/demo.html). + +## Creating Your Own Examples + +Feel free to use these examples as a starting point for your own projects. You can copy and modify them to suit your needs. + +If you create an example that might be useful to others, consider contributing it back to the Brainy project! diff --git a/examples/browser-server-search/README.md b/examples/browser-server-search/README.md new file mode 100644 index 00000000..4427d2ba --- /dev/null +++ b/examples/browser-server-search/README.md @@ -0,0 +1,187 @@ +# Brainy Browser-Server Search Example + +This example demonstrates how to use Brainy in a browser, call a server-hosted version for search, store the results locally, and then perform further searches against the local instance. + +## Overview + +The solution consists of: + +1. A `BrainyServerSearch` class that handles the connection to the server and local storage +2. An HTML interface for testing the functionality +3. Server-side setup using the Brainy cloud wrapper + +This approach allows you to: +- Search a server-hosted Brainy instance from a browser +- Store the search results in a local Brainy instance +- Perform further searches against the local instance without needing to query the server again +- Add data to both local and server instances + +## How It Works + +1. The browser creates a local Brainy instance +2. It connects to the server-hosted Brainy instance using WebSocket +3. When a search is performed: + - The query is sent to the server + - The server returns the search results + - The results are stored in the local Brainy instance + - The results are displayed to the user +4. Subsequent searches can be performed against the local instance +5. A combined search mode first checks the local instance and then queries the server only if needed + +## Setup Instructions + +### Server Setup + +1. Set up the Brainy cloud wrapper: + +```bash +# Clone the repository if you haven't already +git clone https://github.com/soulcraft/brainy.git +cd brainy/cloud-wrapper + +# Install dependencies +npm install --legacy-peer-deps + +# Configure the server +cp .env.example .env +# Edit .env to configure your environment + +# Build and start the server +npm run build +npm run start +``` + +2. Note the WebSocket URL of your server (e.g., `wss://your-server.com/ws` or `ws://localhost:3000/ws` for local development) + +### Client Setup + +1. Copy the example files to your project: + +```bash +cp -r examples/browser-server-search your-project/ +``` + +2. Include the Brainy library in your project: + +```bash +npm install @soulcraft/brainy --legacy-peer-deps +``` + +3. Open the HTML file in a browser or serve it using a local server: + +```bash +# Using a simple HTTP server +cd your-project +npx http-server +``` + +4. Navigate to http://localhost:8080/browser-server-search/ in your browser + +5. Enter the WebSocket URL of your server and start using the example + +## Usage + +### Using the HTML Interface + +1. Enter the WebSocket URL of your Brainy server +2. Click "Connect" to establish a connection +3. Enter a search query and click one of the search buttons: + - "Search Server" - Search the server and store results locally + - "Search Local" - Search only the local instance + - "Search Combined" - Search local first, then server if needed +4. To add data, enter text in the "Add Data" field and click "Add to Both" + +### Using the BrainyServerSearch Class in Your Code + +```javascript +import { BrainyServerSearch } from './index.js'; + +// Create a new instance +const brainySearch = new BrainyServerSearch('wss://your-brainy-server.com/ws'); + +// Initialize and connect +await brainySearch.init(); + +// Search the server and store results locally +const serverResults = await brainySearch.searchServer('machine learning', 5); + +// Search the local instance +const localResults = await brainySearch.searchLocal('machine learning', 5); + +// Perform a combined search +const combinedResults = await brainySearch.searchCombined('neural networks', 5); + +// Add data to both local and server +const id = await brainySearch.add('Deep learning is a subset of machine learning', { + noun: 'Concept', + category: 'AI', + tags: ['deep learning', 'neural networks'] +}); + +// Close the connection when done +await brainySearch.close(); +``` + +## API Reference + +### BrainyServerSearch Class + +#### Constructor + +```javascript +const brainySearch = new BrainyServerSearch(serverUrl); +``` + +- `serverUrl` (string): WebSocket URL of the Brainy server + +#### Methods + +- `init()`: Initialize the local Brainy instance and connect to the server +- `searchServer(query, limit = 10)`: Search the server-hosted Brainy instance, store results locally, and return them +- `searchLocal(query, limit = 10)`: Search the local Brainy instance +- `searchCombined(query, limit = 10)`: Search both server and local instances, combine results, and store server results locally +- `add(data, metadata = {})`: Add data to both local and server instances +- `close()`: Close the connection to the server + +## Advanced Configuration + +### Custom Embedding Function + +You can customize the embedding function used by the local Brainy instance: + +```javascript +import { createSimpleEmbeddingFunction } from '@soulcraft/brainy'; + +// In your code, before calling init(): +brainySearch.setEmbeddingFunction(createSimpleEmbeddingFunction()); +``` + +### Persistent Storage + +To enable persistent storage for the local Brainy instance: + +```javascript +// In your code, before calling init(): +brainySearch.setStorageOptions({ + requestPersistentStorage: true +}); +``` + +## Troubleshooting + +### Connection Issues + +- Ensure the server is running and accessible +- Check that the WebSocket URL is correct +- Verify that your browser supports WebSockets +- Check for CORS issues if the server is on a different domain + +### Search Issues + +- Ensure the server has data to search +- Check that the query is not empty +- Verify that the server is properly configured for search + +## License + +MIT diff --git a/examples/browser-server-search/index.html b/examples/browser-server-search/index.html new file mode 100644 index 00000000..59f2ee4f --- /dev/null +++ b/examples/browser-server-search/index.html @@ -0,0 +1,251 @@ + + + + + + Brainy Browser-Server Search Example + + + +

Brainy Browser-Server Search Example

+ +

+ This example demonstrates how to use Brainy in a browser, call a server-hosted version for search, + store the results locally, and then perform further searches against the local instance. +

+ +
+

Server URL

+ + +
+ +
+

Search

+ + + + +
+ +
+

Add Data

+ + +
+ +
+

Results

+
Connect to a server to begin...
+
+ +
+ + + + + diff --git a/examples/browser-server-search/index.js b/examples/browser-server-search/index.js new file mode 100644 index 00000000..05ba979a --- /dev/null +++ b/examples/browser-server-search/index.js @@ -0,0 +1,278 @@ +// Browser-Server Search Example +// This example demonstrates how to use Brainy in a browser, call a server-hosted version for search, +// store the results locally, and then perform further searches against the local instance. + +import { + BrainyData, + augmentationPipeline, + createConduitAugmentation, + NounType +} from '@soulcraft/brainy'; + +/** + * BrainyServerSearch class + * Provides functionality to search a server-hosted Brainy instance and store results locally + */ +class BrainyServerSearch { + constructor(serverUrl) { + this.serverUrl = serverUrl; + this.localDb = null; + this.wsConduit = null; + this.connection = null; + this.isInitialized = false; + } + + /** + * Initialize the local Brainy instance and connect to the server + */ + async init() { + if (this.isInitialized) { + return; + } + + try { + // Initialize local Brainy instance + this.localDb = new BrainyData(); + await this.localDb.init(); + + // Create a WebSocket conduit augmentation + this.wsConduit = await createConduitAugmentation('websocket', 'server-search-conduit'); + + // Register the augmentation with the pipeline + augmentationPipeline.register(this.wsConduit); + + // Connect to the server + const connectionResult = await augmentationPipeline.executeConduitPipeline( + 'establishConnection', + [this.serverUrl, { protocols: 'brainy-sync' }] + ); + + if (connectionResult[0] && (await connectionResult[0]).success) { + this.connection = (await connectionResult[0]).data; + console.log('Connected to server:', this.serverUrl); + this.isInitialized = true; + } else { + throw new Error('Failed to connect to server'); + } + } catch (error) { + console.error('Failed to initialize BrainyServerSearch:', error); + throw error; + } + } + + /** + * Search the server-hosted Brainy instance, store results locally, and return them + * @param {string} query - The search query + * @param {number} limit - Maximum number of results to return + * @returns {Promise} - Search results + */ + async searchServer(query, limit = 10) { + await this.ensureInitialized(); + + try { + // Create a search request + const readResult = await augmentationPipeline.executeConduitPipeline( + 'readData', + [{ + connectionId: this.connection.connectionId, + query: { + type: 'search', + query: query, + limit: limit + } + }] + ); + + if (readResult[0] && (await readResult[0]).success) { + const searchResults = (await readResult[0]).data; + + // Store the results in the local Brainy instance + for (const result of searchResults) { + // Check if the noun already exists in the local database + const existingNoun = await this.localDb.get(result.id); + + if (!existingNoun) { + // Add the noun to the local database + await this.localDb.add(result.vector, result.metadata); + } + } + + return searchResults; + } else { + const error = readResult[0] ? (await readResult[0]).error : 'Unknown error'; + throw new Error(`Failed to search server: ${error}`); + } + } catch (error) { + console.error('Error searching server:', error); + throw error; + } + } + + /** + * Search the local Brainy instance + * @param {string} query - The search query + * @param {number} limit - Maximum number of results to return + * @returns {Promise} - Search results + */ + async searchLocal(query, limit = 10) { + await this.ensureInitialized(); + + try { + return await this.localDb.searchText(query, limit); + } catch (error) { + console.error('Error searching local database:', error); + throw error; + } + } + + /** + * Search both server and local instances, combine results, and store server results locally + * @param {string} query - The search query + * @param {number} limit - Maximum number of results to return + * @returns {Promise} - Combined search results + */ + async searchCombined(query, limit = 10) { + await this.ensureInitialized(); + + try { + // Search local first + const localResults = await this.searchLocal(query, limit); + + // If we have enough local results, return them + if (localResults.length >= limit) { + return localResults; + } + + // Otherwise, search server for additional results + const serverResults = await this.searchServer(query, limit - localResults.length); + + // Combine results, removing duplicates + const combinedResults = [...localResults]; + const localIds = new Set(localResults.map(r => r.id)); + + for (const result of serverResults) { + if (!localIds.has(result.id)) { + combinedResults.push(result); + } + } + + return combinedResults; + } catch (error) { + console.error('Error performing combined search:', error); + throw error; + } + } + + /** + * Add data to both local and server instances + * @param {string|Array} data - Text or vector to add + * @param {Object} metadata - Metadata for the data + * @returns {Promise} - ID of the added data + */ + async add(data, metadata = {}) { + await this.ensureInitialized(); + + try { + // Add to local first + const id = await this.localDb.add(data, metadata); + + // Get the vector and metadata + const noun = await this.localDb.get(id); + + // Add to server + await augmentationPipeline.executeConduitPipeline( + 'writeData', + [{ + connectionId: this.connection.connectionId, + data: { + type: 'addNoun', + vector: noun.vector, + metadata: noun.metadata + } + }] + ); + + return id; + } catch (error) { + console.error('Error adding data:', error); + throw error; + } + } + + /** + * Ensure the instance is initialized + */ + async ensureInitialized() { + if (!this.isInitialized) { + await this.init(); + } + } + + /** + * Close the connection to the server + */ + async close() { + if (this.connection) { + try { + await this.wsConduit.closeWebSocket(this.connection.connectionId); + this.connection = null; + } catch (error) { + console.error('Error closing connection:', error); + } + } + + this.isInitialized = false; + } +} + +// Example usage +async function runExample() { + // Create a BrainyServerSearch instance + const brainySearch = new BrainyServerSearch('wss://your-brainy-server.com/ws'); + + try { + // Initialize + await brainySearch.init(); + + // Search the server and store results locally + console.log('Searching server for "machine learning"...'); + const serverResults = await brainySearch.searchServer('machine learning', 5); + console.log('Server results:', serverResults); + + // Now search locally - this should return the results we just stored + console.log('Searching local database for "machine learning"...'); + const localResults = await brainySearch.searchLocal('machine learning', 5); + console.log('Local results:', localResults); + + // Search for something related but different + console.log('Searching local database for "artificial intelligence"...'); + const aiResults = await brainySearch.searchLocal('artificial intelligence', 5); + console.log('AI results:', aiResults); + + // Perform a combined search + console.log('Performing combined search for "neural networks"...'); + const combinedResults = await brainySearch.searchCombined('neural networks', 5); + console.log('Combined results:', combinedResults); + + // Add new data to both local and server + console.log('Adding new data...'); + const id = await brainySearch.add('Deep learning is a subset of machine learning', { + noun: NounType.Concept, + category: 'AI', + tags: ['deep learning', 'neural networks'] + }); + console.log('Added data with ID:', id); + + // Close the connection + await brainySearch.close(); + + } catch (error) { + console.error('Example failed:', error); + } +} + +// In a browser environment, you would call this when the page loads +// runExample(); + +// Export for use in other modules +export { BrainyServerSearch }; diff --git a/examples/serverSearchAugmentationExample.js b/examples/serverSearchAugmentationExample.js new file mode 100644 index 00000000..b24d5a02 --- /dev/null +++ b/examples/serverSearchAugmentationExample.js @@ -0,0 +1,346 @@ +/** + * Server Search Augmentation Example + * + * This example demonstrates how to use the ServerSearchConduitAugmentation and + * ServerSearchActivationAugmentation to search a server-hosted Brainy instance, + * store results locally, and perform further searches against the local instance. + */ + +import { + BrainyData, + augmentationPipeline, + AugmentationType, + NounType +} from '@soulcraft/brainy' + +// Import the server search augmentations +import { + ServerSearchConduitAugmentation, + ServerSearchActivationAugmentation, + createServerSearchAugmentations +} from '../src/augmentations/serverSearchAugmentations.js' + +/** + * Example 1: Using the factory function + * + * This is the simplest way to use the server search augmentations. + * The factory function creates both augmentations, links them together, + * and connects to the server. + */ +async function example1() { + console.log('Example 1: Using the factory function') + + try { + // Create the augmentations and connect to the server + const { conduit, activation, connection } = await createServerSearchAugmentations( + 'wss://your-brainy-server.com/ws', + { protocols: 'brainy-sync' } + ) + + // Register the augmentations with the pipeline + augmentationPipeline.register(conduit) + augmentationPipeline.register(activation) + + console.log('Connected to server with connection ID:', connection.connectionId) + + // Search the server and store results locally + console.log('Searching server for "machine learning"...') + const serverSearchResult = await conduit.searchServer( + connection.connectionId, + 'machine learning', + 5 + ) + + if (serverSearchResult.success) { + console.log('Server search results:', serverSearchResult.data) + } else { + console.error('Server search failed:', serverSearchResult.error) + } + + // Now search locally - this should return the results we just stored + console.log('Searching local database for "machine learning"...') + const localSearchResult = await conduit.searchLocal('machine learning', 5) + + if (localSearchResult.success) { + console.log('Local search results:', localSearchResult.data) + } else { + console.error('Local search failed:', localSearchResult.error) + } + + // Perform a combined search + console.log('Performing combined search for "neural networks"...') + const combinedSearchResult = await conduit.searchCombined( + connection.connectionId, + 'neural networks', + 5 + ) + + if (combinedSearchResult.success) { + console.log('Combined search results:', combinedSearchResult.data) + } else { + console.error('Combined search failed:', combinedSearchResult.error) + } + + // Add data to both local and server + console.log('Adding data to both local and server...') + const addResult = await conduit.addToBoth( + connection.connectionId, + 'Deep learning is a subset of machine learning', + { + noun: NounType.Concept, + category: 'AI', + tags: ['deep learning', 'neural networks'] + } + ) + + if (addResult.success) { + console.log('Added data with ID:', addResult.data) + } else { + console.error('Failed to add data:', addResult.error) + } + + } catch (error) { + console.error('Example 1 failed:', error) + } +} + +/** + * Example 2: Using the activation augmentation + * + * This example demonstrates how to use the activation augmentation + * to trigger actions related to server search. + */ +async function example2() { + console.log('\nExample 2: Using the activation augmentation') + + try { + // Create the augmentations and connect to the server + const { conduit, activation, connection } = await createServerSearchAugmentations( + 'wss://your-brainy-server.com/ws', + { protocols: 'brainy-sync' } + ) + + // Register the augmentations with the pipeline + augmentationPipeline.register(conduit) + augmentationPipeline.register(activation) + + console.log('Connected to server with connection ID:', connection.connectionId) + + // Use the activation augmentation to search the server + console.log('Using activation to search server for "machine learning"...') + const serverSearchAction = activation.triggerAction('searchServer', { + connectionId: connection.connectionId, + query: 'machine learning', + limit: 5 + }) + + if (serverSearchAction.success) { + // The data property contains a promise that will resolve to the search results + const serverSearchResult = await serverSearchAction.data + console.log('Server search results:', serverSearchResult) + } else { + console.error('Server search action failed:', serverSearchAction.error) + } + + // Use the activation augmentation to search locally + console.log('Using activation to search local database for "machine learning"...') + const localSearchAction = activation.triggerAction('searchLocal', { + query: 'machine learning', + limit: 5 + }) + + if (localSearchAction.success) { + const localSearchResult = await localSearchAction.data + console.log('Local search results:', localSearchResult) + } else { + console.error('Local search action failed:', localSearchAction.error) + } + + // Use the activation augmentation to perform a combined search + console.log('Using activation to perform combined search for "neural networks"...') + const combinedSearchAction = activation.triggerAction('searchCombined', { + connectionId: connection.connectionId, + query: 'neural networks', + limit: 5 + }) + + if (combinedSearchAction.success) { + const combinedSearchResult = await combinedSearchAction.data + console.log('Combined search results:', combinedSearchResult) + } else { + console.error('Combined search action failed:', combinedSearchAction.error) + } + + // Use the activation augmentation to add data to both local and server + console.log('Using activation to add data to both local and server...') + const addAction = activation.triggerAction('addToBoth', { + connectionId: connection.connectionId, + data: 'Deep learning is a subset of machine learning', + metadata: { + noun: NounType.Concept, + category: 'AI', + tags: ['deep learning', 'neural networks'] + } + }) + + if (addAction.success) { + const addResult = await addAction.data + console.log('Added data with ID:', addResult) + } else { + console.error('Add action failed:', addAction.error) + } + + } catch (error) { + console.error('Example 2 failed:', error) + } +} + +/** + * Example 3: Using the augmentation pipeline + * + * This example demonstrates how to use the augmentation pipeline + * to execute the conduit and activation augmentations. + */ +async function example3() { + console.log('\nExample 3: Using the augmentation pipeline') + + try { + // Create the augmentations and connect to the server + const { conduit, activation, connection } = await createServerSearchAugmentations( + 'wss://your-brainy-server.com/ws', + { protocols: 'brainy-sync' } + ) + + // Register the augmentations with the pipeline + augmentationPipeline.register(conduit) + augmentationPipeline.register(activation) + + console.log('Connected to server with connection ID:', connection.connectionId) + + // Use the augmentation pipeline to search the server + console.log('Using pipeline to search server...') + const conduitResults = await augmentationPipeline.executeConduitPipeline( + 'searchServer', + [connection.connectionId, 'machine learning', 5] + ) + + if (conduitResults.length > 0 && (await conduitResults[0]).success) { + console.log('Server search results:', (await conduitResults[0]).data) + } else { + console.error('Server search failed') + } + + // Use the augmentation pipeline to trigger the search action + console.log('Using pipeline to trigger search action...') + const activationResults = await augmentationPipeline.executeActivationPipeline( + 'triggerAction', + ['searchLocal', { query: 'machine learning', limit: 5 }] + ) + + if (activationResults.length > 0 && (await activationResults[0]).success) { + const actionResult = (await activationResults[0]).data + if (actionResult.success) { + const searchResult = await actionResult.data + console.log('Local search results:', searchResult) + } + } else { + console.error('Search action failed') + } + + } catch (error) { + console.error('Example 3 failed:', error) + } +} + +/** + * Example 4: Creating and using the augmentations manually + * + * This example demonstrates how to create and use the augmentations + * without using the factory function. + */ +async function example4() { + console.log('\nExample 4: Creating and using the augmentations manually') + + try { + // Create a local Brainy instance + const localDb = new BrainyData() + await localDb.init() + + // Create the conduit augmentation + const conduit = new ServerSearchConduitAugmentation('manual-server-search-conduit') + conduit.setLocalDb(localDb) + await conduit.initialize() + + // Create the activation augmentation + const activation = new ServerSearchActivationAugmentation('manual-server-search-activation') + activation.setConduitAugmentation(conduit) + await activation.initialize() + + // Register the augmentations with the pipeline + augmentationPipeline.register(conduit) + augmentationPipeline.register(activation) + + // Connect to the server + console.log('Connecting to server...') + const connectionResult = await conduit.establishConnection( + 'wss://your-brainy-server.com/ws', + { protocols: 'brainy-sync' } + ) + + if (!connectionResult.success || !connectionResult.data) { + throw new Error(`Failed to connect to server: ${connectionResult.error}`) + } + + const connection = connectionResult.data + console.log('Connected to server with connection ID:', connection.connectionId) + + // Store the connection in the activation augmentation + activation.storeConnection(connection.connectionId, connection) + + // Search the server + console.log('Searching server for "machine learning"...') + const serverSearchResult = await conduit.searchServer( + connection.connectionId, + 'machine learning', + 5 + ) + + if (serverSearchResult.success) { + console.log('Server search results:', serverSearchResult.data) + } else { + console.error('Server search failed:', serverSearchResult.error) + } + + } catch (error) { + console.error('Example 4 failed:', error) + } +} + +/** + * Run all examples + */ +async function runExamples() { + // Initialize the augmentation pipeline + await augmentationPipeline.initialize() + + // Run the examples + await example1() + await example2() + await example3() + await example4() + + // Shut down the augmentation pipeline + await augmentationPipeline.shutDown() +} + +// Run the examples +// runExamples().catch(console.error) + +// Export for use in other modules +export { + example1, + example2, + example3, + example4, + runExamples +} diff --git a/src/augmentations/README.md b/src/augmentations/README.md new file mode 100644 index 00000000..c9bb1f36 --- /dev/null +++ b/src/augmentations/README.md @@ -0,0 +1,230 @@ +# Brainy Augmentations + +This directory contains the augmentation implementations for Brainy. Augmentations are pluggable components that extend Brainy's functionality in various ways. + +## Available Augmentations + +### Conduit Augmentations + +Conduit augmentations provide data synchronization between Brainy instances. + +#### WebSocketConduitAugmentation + +A conduit augmentation that syncs Brainy instances using WebSockets. This is used for syncing between browsers and servers, or between servers. + +```javascript +import { createConduitAugmentation, augmentationPipeline } from '@soulcraft/brainy' + +// Create a WebSocket conduit augmentation +const wsConduit = await createConduitAugmentation('websocket', 'my-websocket-sync') + +// Register the augmentation with the pipeline +augmentationPipeline.register(wsConduit) + +// Connect to another Brainy instance +const connectionResult = await wsConduit.establishConnection( + 'wss://your-websocket-server.com/brainy-sync', + { protocols: 'brainy-sync' } +) +``` + +#### WebRTCConduitAugmentation + +A conduit augmentation that syncs Brainy instances using WebRTC. This is used for direct peer-to-peer syncing between browsers. + +```javascript +import { createConduitAugmentation, augmentationPipeline } from '@soulcraft/brainy' + +// Create a WebRTC conduit augmentation +const webrtcConduit = await createConduitAugmentation('webrtc', 'my-webrtc-sync') + +// Register the augmentation with the pipeline +augmentationPipeline.register(webrtcConduit) + +// Connect to a peer +const connectionResult = await webrtcConduit.establishConnection( + 'peer-id-to-connect-to', + { + signalServerUrl: 'wss://your-signal-server.com', + localPeerId: 'my-peer-id', + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + } +) +``` + +#### ServerSearchConduitAugmentation + +A specialized conduit augmentation that provides functionality for searching a server-hosted Brainy instance and storing results locally. This allows you to: + +- Search a server-hosted Brainy instance from a browser +- Store the search results in a local Brainy instance +- Perform further searches against the local instance without needing to query the server again +- Add data to both local and server instances + +```javascript +import { + ServerSearchConduitAugmentation, + createServerSearchAugmentations, + augmentationPipeline +} from '@soulcraft/brainy' + +// Using the factory function (recommended) +const { conduit, activation, connection } = await createServerSearchAugmentations( + 'wss://your-brainy-server.com/ws', + { protocols: 'brainy-sync' } +) + +// Register the augmentations with the pipeline +augmentationPipeline.register(conduit) +augmentationPipeline.register(activation) + +// Search the server and store results locally +const serverSearchResult = await conduit.searchServer( + connection.connectionId, + 'your search query', + 5 // limit +) + +// Search the local instance +const localSearchResult = await conduit.searchLocal('your search query', 5) + +// Perform a combined search (local first, then server if needed) +const combinedSearchResult = await conduit.searchCombined( + connection.connectionId, + 'your search query', + 5 +) + +// Add data to both local and server +const addResult = await conduit.addToBoth( + connection.connectionId, + 'Text to add', + { /* metadata */ } +) +``` + +### Activation Augmentations + +Activation augmentations dictate how Brainy initiates actions, responses, or data manipulations. + +#### ServerSearchActivationAugmentation + +An activation augmentation that provides actions for server search functionality. This works in conjunction with the ServerSearchConduitAugmentation to provide a complete solution for browser-server search. + +```javascript +import { + ServerSearchActivationAugmentation, + createServerSearchAugmentations, + augmentationPipeline +} from '@soulcraft/brainy' + +// Using the factory function (recommended) +const { conduit, activation, connection } = await createServerSearchAugmentations( + 'wss://your-brainy-server.com/ws', + { protocols: 'brainy-sync' } +) + +// Register the augmentations with the pipeline +augmentationPipeline.register(conduit) +augmentationPipeline.register(activation) + +// Use the activation augmentation to search the server +const serverSearchAction = activation.triggerAction('searchServer', { + connectionId: connection.connectionId, + query: 'your search query', + limit: 5 +}) + +if (serverSearchAction.success) { + // The data property contains a promise that will resolve to the search results + const serverSearchResult = await serverSearchAction.data + console.log('Server search results:', serverSearchResult) +} + +// Other available actions: +// - 'connectToServer': Connect to a server +// - 'searchLocal': Search the local instance +// - 'searchCombined': Search both local and server +// - 'addToBoth': Add data to both local and server +``` + +## Using the Augmentation Pipeline + +The augmentation pipeline provides a way to execute augmentations based on their type. + +```javascript +import { augmentationPipeline } from '@soulcraft/brainy' + +// Execute a conduit augmentation +const conduitResults = await augmentationPipeline.executeConduitPipeline( + 'methodName', + [arg1, arg2, ...], + { /* options */ } +) + +// Execute an activation augmentation +const activationResults = await augmentationPipeline.executeActivationPipeline( + 'methodName', + [arg1, arg2, ...], + { /* options */ } +) +``` + +## Creating Custom Augmentations + +To create a custom augmentation, implement one of the augmentation interfaces: + +- `ISenseAugmentation`: For processing raw data +- `IConduitAugmentation`: For data synchronization +- `ICognitionAugmentation`: For reasoning and inference +- `IMemoryAugmentation`: For data storage +- `IPerceptionAugmentation`: For data interpretation and visualization +- `IDialogAugmentation`: For natural language processing +- `IActivationAugmentation`: For triggering actions + +Example: + +```javascript +import { AugmentationType, IActivationAugmentation } from '@soulcraft/brainy' + +class MyCustomActivation implements IActivationAugmentation { + readonly name = 'my-custom-activation' + readonly description = 'My custom activation augmentation' + enabled = true + + getType(): AugmentationType { + return AugmentationType.ACTIVATION + } + + async initialize(): Promise { + // Initialization code + } + + async shutDown(): Promise { + // Cleanup code + } + + async getStatus(): Promise<'active' | 'inactive' | 'error'> { + return 'active' + } + + triggerAction(actionName: string, parameters?: Record): AugmentationResponse { + // Implementation + } + + generateOutput(knowledgeId: string, format: string): AugmentationResponse> { + // Implementation + } + + interactExternal(systemId: string, payload: Record): AugmentationResponse { + // Implementation + } +} +``` + +## Examples + +For complete examples of using augmentations, see the `examples` directory: + +- `conduitAugmentationExample.js`: Demonstrates how to use conduit augmentations +- `serverSearchAugmentationExample.js`: Demonstrates how to use server search augmentations diff --git a/src/augmentations/serverSearchAugmentations.ts b/src/augmentations/serverSearchAugmentations.ts new file mode 100644 index 00000000..53537e22 --- /dev/null +++ b/src/augmentations/serverSearchAugmentations.ts @@ -0,0 +1,666 @@ +/** + * Server Search Augmentations + * + * This file implements conduit and activation augmentations for browser-server search functionality. + * It allows Brainy to search a server-hosted instance and store results locally. + */ + +import { + AugmentationType, + IConduitAugmentation, + IActivationAugmentation, + IWebSocketSupport, + AugmentationResponse, + WebSocketConnection +} from '../types/augmentations.js' +import { WebSocketConduitAugmentation } from './conduitAugmentations.js' +import { v4 as uuidv4 } from 'uuid' +import { BrainyData } from '../brainyData.js' + +/** + * ServerSearchConduitAugmentation + * + * A specialized conduit augmentation that provides functionality for searching + * a server-hosted Brainy instance and storing results locally. + */ +export class ServerSearchConduitAugmentation extends WebSocketConduitAugmentation { + private localDb: BrainyData | null = null + + constructor(name: string = 'server-search-conduit') { + super(name) + this.description = 'Conduit augmentation for server-hosted Brainy search' + } + + /** + * Initialize the augmentation + */ + async initialize(): Promise { + if (this.isInitialized) { + return + } + + try { + // Initialize the base conduit + await super.initialize() + + // Initialize local Brainy instance if not provided + if (!this.localDb) { + this.localDb = new BrainyData() + await this.localDb.init() + } + + this.isInitialized = true + } catch (error) { + console.error(`Failed to initialize ${this.name}:`, error) + throw new Error(`Failed to initialize ${this.name}: ${error}`) + } + } + + /** + * Set the local Brainy instance + * @param db The Brainy instance to use for local storage + */ + setLocalDb(db: BrainyData): void { + this.localDb = db + } + + /** + * Get the local Brainy instance + * @returns The local Brainy instance + */ + getLocalDb(): BrainyData | null { + return this.localDb + } + + /** + * Search the server-hosted Brainy instance and store results locally + * @param connectionId The ID of the established connection + * @param query The search query + * @param limit Maximum number of results to return + * @returns Search results + */ + async searchServer( + connectionId: string, + query: string, + limit: number = 10 + ): Promise> { + await this.ensureInitialized() + + try { + // Create a search request + const readResult = await this.readData({ + connectionId, + query: { + type: 'search', + query, + limit + } + }) + + if (readResult.success && readResult.data) { + const searchResults = readResult.data as any[] + + // Store the results in the local Brainy instance + if (this.localDb) { + for (const result of searchResults) { + // Check if the noun already exists in the local database + const existingNoun = await this.localDb.get(result.id) + + if (!existingNoun) { + // Add the noun to the local database + await this.localDb.add(result.vector, result.metadata) + } + } + } + + return { + success: true, + data: searchResults + } + } else { + return { + success: false, + data: null, + error: readResult.error || 'Unknown error searching server' + } + } + } catch (error) { + console.error('Error searching server:', error) + return { + success: false, + data: null, + error: `Error searching server: ${error}` + } + } + } + + /** + * Search the local Brainy instance + * @param query The search query + * @param limit Maximum number of results to return + * @returns Search results + */ + async searchLocal( + query: string, + limit: number = 10 + ): Promise> { + await this.ensureInitialized() + + try { + if (!this.localDb) { + return { + success: false, + data: null, + error: 'Local database not initialized' + } + } + + const results = await this.localDb.searchText(query, limit) + + return { + success: true, + data: results + } + } catch (error) { + console.error('Error searching local database:', error) + return { + success: false, + data: null, + error: `Error searching local database: ${error}` + } + } + } + + /** + * Search both server and local instances, combine results, and store server results locally + * @param connectionId The ID of the established connection + * @param query The search query + * @param limit Maximum number of results to return + * @returns Combined search results + */ + async searchCombined( + connectionId: string, + query: string, + limit: number = 10 + ): Promise> { + await this.ensureInitialized() + + try { + // Search local first + const localSearchResult = await this.searchLocal(query, limit) + + if (!localSearchResult.success) { + return localSearchResult + } + + const localResults = localSearchResult.data as any[] + + // If we have enough local results, return them + if (localResults.length >= limit) { + return localSearchResult + } + + // Otherwise, search server for additional results + const serverSearchResult = await this.searchServer( + connectionId, + query, + limit - localResults.length + ) + + if (!serverSearchResult.success) { + // If server search fails, return local results + return localSearchResult + } + + const serverResults = serverSearchResult.data as any[] + + // Combine results, removing duplicates + const combinedResults = [...localResults] + const localIds = new Set(localResults.map(r => r.id)) + + for (const result of serverResults) { + if (!localIds.has(result.id)) { + combinedResults.push(result) + } + } + + return { + success: true, + data: combinedResults + } + } catch (error) { + console.error('Error performing combined search:', error) + return { + success: false, + data: null, + error: `Error performing combined search: ${error}` + } + } + } + + /** + * Add data to both local and server instances + * @param connectionId The ID of the established connection + * @param data Text or vector to add + * @param metadata Metadata for the data + * @returns ID of the added data + */ + async addToBoth( + connectionId: string, + data: string | any[], + metadata: any = {} + ): Promise> { + await this.ensureInitialized() + + try { + if (!this.localDb) { + return { + success: false, + data: '', + error: 'Local database not initialized' + } + } + + // Add to local first + const id = await this.localDb.add(data, metadata) + + // Get the vector and metadata + const noun = await this.localDb.get(id) + + if (!noun) { + return { + success: false, + data: '', + error: 'Failed to retrieve newly created noun' + } + } + + // Add to server + const writeResult = await this.writeData({ + connectionId, + data: { + type: 'addNoun', + vector: noun.vector, + metadata: noun.metadata + } + }) + + if (!writeResult.success) { + return { + success: true, + data: id, + error: `Added locally but failed to add to server: ${writeResult.error}` + } + } + + return { + success: true, + data: id + } + } catch (error) { + console.error('Error adding data to both:', error) + return { + success: false, + data: '', + error: `Error adding data to both: ${error}` + } + } + } +} + +/** + * ServerSearchActivationAugmentation + * + * An activation augmentation that provides actions for server search functionality. + */ +export class ServerSearchActivationAugmentation implements IActivationAugmentation { + readonly name: string + readonly description: string + enabled: boolean = true + private isInitialized = false + private conduitAugmentation: ServerSearchConduitAugmentation | null = null + private connections: Map = new Map() + + constructor(name: string = 'server-search-activation') { + this.name = name + this.description = 'Activation augmentation for server-hosted Brainy search' + } + + getType(): AugmentationType { + return AugmentationType.ACTIVATION + } + + /** + * Initialize the augmentation + */ + async initialize(): Promise { + if (this.isInitialized) { + return + } + + this.isInitialized = true + } + + /** + * Shut down the augmentation + */ + async shutDown(): Promise { + this.isInitialized = false + } + + /** + * Get the status of the augmentation + */ + async getStatus(): Promise<'active' | 'inactive' | 'error'> { + return this.isInitialized ? 'active' : 'inactive' + } + + /** + * Set the conduit augmentation to use for server search + * @param conduit The ServerSearchConduitAugmentation to use + */ + setConduitAugmentation(conduit: ServerSearchConduitAugmentation): void { + this.conduitAugmentation = conduit + } + + /** + * Store a connection for later use + * @param connectionId The ID to use for the connection + * @param connection The WebSocket connection + */ + storeConnection(connectionId: string, connection: WebSocketConnection): void { + this.connections.set(connectionId, connection) + } + + /** + * Get a stored connection + * @param connectionId The ID of the connection to retrieve + * @returns The WebSocket connection + */ + getConnection(connectionId: string): WebSocketConnection | undefined { + return this.connections.get(connectionId) + } + + /** + * Trigger an action based on a processed command or internal state + * @param actionName The name of the action to trigger + * @param parameters Optional parameters for the action + */ + triggerAction( + actionName: string, + parameters?: Record + ): AugmentationResponse { + if (!this.conduitAugmentation) { + return { + success: false, + data: null, + error: 'Conduit augmentation not set' + } + } + + // Handle different actions + switch (actionName) { + case 'connectToServer': + return this.handleConnectToServer(parameters || {}) + case 'searchServer': + return this.handleSearchServer(parameters || {}) + case 'searchLocal': + return this.handleSearchLocal(parameters || {}) + case 'searchCombined': + return this.handleSearchCombined(parameters || {}) + case 'addToBoth': + return this.handleAddToBoth(parameters || {}) + default: + return { + success: false, + data: null, + error: `Unknown action: ${actionName}` + } + } + } + + /** + * Handle the connectToServer action + * @param parameters Action parameters + */ + private handleConnectToServer( + parameters: Record + ): AugmentationResponse { + const serverUrl = parameters.serverUrl as string + const protocols = parameters.protocols as string | string[] | undefined + + if (!serverUrl) { + return { + success: false, + data: null, + error: 'serverUrl parameter is required' + } + } + + // Return a promise that will be resolved when the connection is established + return { + success: true, + data: this.conduitAugmentation!.establishConnection(serverUrl, { + protocols + }) + } + } + + /** + * Handle the searchServer action + * @param parameters Action parameters + */ + private handleSearchServer( + parameters: Record + ): AugmentationResponse { + const connectionId = parameters.connectionId as string + const query = parameters.query as string + const limit = parameters.limit as number || 10 + + if (!connectionId) { + return { + success: false, + data: null, + error: 'connectionId parameter is required' + } + } + + if (!query) { + return { + success: false, + data: null, + error: 'query parameter is required' + } + } + + // Return a promise that will be resolved when the search is complete + return { + success: true, + data: this.conduitAugmentation!.searchServer(connectionId, query, limit) + } + } + + /** + * Handle the searchLocal action + * @param parameters Action parameters + */ + private handleSearchLocal( + parameters: Record + ): AugmentationResponse { + const query = parameters.query as string + const limit = parameters.limit as number || 10 + + if (!query) { + return { + success: false, + data: null, + error: 'query parameter is required' + } + } + + // Return a promise that will be resolved when the search is complete + return { + success: true, + data: this.conduitAugmentation!.searchLocal(query, limit) + } + } + + /** + * Handle the searchCombined action + * @param parameters Action parameters + */ + private handleSearchCombined( + parameters: Record + ): AugmentationResponse { + const connectionId = parameters.connectionId as string + const query = parameters.query as string + const limit = parameters.limit as number || 10 + + if (!connectionId) { + return { + success: false, + data: null, + error: 'connectionId parameter is required' + } + } + + if (!query) { + return { + success: false, + data: null, + error: 'query parameter is required' + } + } + + // Return a promise that will be resolved when the search is complete + return { + success: true, + data: this.conduitAugmentation!.searchCombined(connectionId, query, limit) + } + } + + /** + * Handle the addToBoth action + * @param parameters Action parameters + */ + private handleAddToBoth( + parameters: Record + ): AugmentationResponse { + const connectionId = parameters.connectionId as string + const data = parameters.data + const metadata = parameters.metadata || {} + + if (!connectionId) { + return { + success: false, + data: null, + error: 'connectionId parameter is required' + } + } + + if (!data) { + return { + success: false, + data: null, + error: 'data parameter is required' + } + } + + // Return a promise that will be resolved when the add is complete + return { + success: true, + data: this.conduitAugmentation!.addToBoth(connectionId, data as any, metadata as any) + } + } + + /** + * Generates an expressive output or response from Brainy + * @param knowledgeId The identifier of the knowledge to express + * @param format The desired output format (e.g., 'text', 'json') + */ + generateOutput( + knowledgeId: string, + format: string + ): AugmentationResponse> { + // This method is not used for server search functionality + return { + success: false, + data: '', + error: 'generateOutput is not implemented for ServerSearchActivationAugmentation' + } + } + + /** + * Interacts with an external system or API + * @param systemId The identifier of the external system + * @param payload The data to send to the external system + */ + interactExternal( + systemId: string, + payload: Record + ): AugmentationResponse { + // This method is not used for server search functionality + return { + success: false, + data: null, + error: 'interactExternal is not implemented for ServerSearchActivationAugmentation' + } + } +} + +/** + * Factory function to create server search augmentations + * @param serverUrl The URL of the server to connect to + * @param options Additional options + * @returns An object containing the created augmentations + */ +export async function createServerSearchAugmentations( + serverUrl: string, + options: { + conduitName?: string, + activationName?: string, + protocols?: string | string[], + localDb?: BrainyData + } = {} +): Promise<{ + conduit: ServerSearchConduitAugmentation, + activation: ServerSearchActivationAugmentation, + connection: WebSocketConnection +}> { + // Create the conduit augmentation + const conduit = new ServerSearchConduitAugmentation(options.conduitName) + await conduit.initialize() + + // Set the local database if provided + if (options.localDb) { + conduit.setLocalDb(options.localDb) + } + + // Create the activation augmentation + const activation = new ServerSearchActivationAugmentation(options.activationName) + await activation.initialize() + + // Link the augmentations + activation.setConduitAugmentation(conduit) + + // Connect to the server + const connectionResult = await conduit.establishConnection( + serverUrl, + { protocols: options.protocols } + ) + + if (!connectionResult.success || !connectionResult.data) { + throw new Error(`Failed to connect to server: ${connectionResult.error}`) + } + + const connection = connectionResult.data + + // Store the connection in the activation augmentation + activation.storeConnection(connection.connectionId, connection) + + return { + conduit, + activation, + connection + } +}