feat: add browser-server search example with server search augmentations
This commit is contained in:
parent
bf3d9a055b
commit
a206b9926d
7 changed files with 2021 additions and 0 deletions
666
src/augmentations/serverSearchAugmentations.ts
Normal file
666
src/augmentations/serverSearchAugmentations.ts
Normal file
|
|
@ -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<void> {
|
||||
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<AugmentationResponse<unknown>> {
|
||||
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<AugmentationResponse<unknown>> {
|
||||
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<AugmentationResponse<unknown>> {
|
||||
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<AugmentationResponse<string>> {
|
||||
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<string, WebSocketConnection> = 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<void> {
|
||||
if (this.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isInitialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the augmentation
|
||||
*/
|
||||
async shutDown(): Promise<void> {
|
||||
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<string, unknown>
|
||||
): AugmentationResponse<unknown> {
|
||||
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<string, unknown>
|
||||
): AugmentationResponse<unknown> {
|
||||
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<string, unknown>
|
||||
): AugmentationResponse<unknown> {
|
||||
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<string, unknown>
|
||||
): AugmentationResponse<unknown> {
|
||||
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<string, unknown>
|
||||
): AugmentationResponse<unknown> {
|
||||
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<string, unknown>
|
||||
): AugmentationResponse<unknown> {
|
||||
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<string | Record<string, unknown>> {
|
||||
// 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<string, unknown>
|
||||
): AugmentationResponse<unknown> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue