Complete BZZZ functionality port to CHORUS

🎭 CHORUS now contains full BZZZ functionality adapted for containers

Core systems ported:
- P2P networking (libp2p with DHT and PubSub)
- Task coordination (COOEE protocol)
- HMMM collaborative reasoning
- SHHH encryption and security
- SLURP admin election system
- UCXL content addressing
- UCXI server integration
- Hypercore logging system
- Health monitoring and graceful shutdown
- License validation with KACHING

Container adaptations:
- Environment variable configuration (no YAML files)
- Container-optimized logging to stdout/stderr
- Auto-generated agent IDs for container deployments
- Docker-first architecture

All proven BZZZ P2P protocols, AI integration, and collaboration
features are now available in containerized form.

Next: Build and test container deployment.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-02 20:02:37 +10:00
parent 7c6cbd562a
commit 543ab216f9
224 changed files with 86331 additions and 186 deletions

View File

@@ -0,0 +1,663 @@
package storage
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
"github.com/blevesearch/bleve/v2/analysis/lang/en"
"github.com/blevesearch/bleve/v2/mapping"
"chorus.services/bzzz/pkg/ucxl"
slurpContext "chorus.services/bzzz/pkg/slurp/context"
)
// IndexManagerImpl implements the IndexManager interface using Bleve
type IndexManagerImpl struct {
mu sync.RWMutex
indexes map[string]bleve.Index
stats map[string]*IndexStatistics
basePath string
nodeID string
options *IndexManagerOptions
}
// IndexManagerOptions configures index manager behavior
type IndexManagerOptions struct {
DefaultAnalyzer string `json:"default_analyzer"`
MaxDocumentSize int64 `json:"max_document_size"`
RefreshInterval time.Duration `json:"refresh_interval"`
OptimizeInterval time.Duration `json:"optimize_interval"`
EnableHighlighting bool `json:"enable_highlighting"`
EnableFaceting bool `json:"enable_faceting"`
BatchSize int `json:"batch_size"`
MaxResults int `json:"max_results"`
}
// DefaultIndexManagerOptions returns sensible defaults
func DefaultIndexManagerOptions() *IndexManagerOptions {
return &IndexManagerOptions{
DefaultAnalyzer: "standard",
MaxDocumentSize: 10 * 1024 * 1024, // 10MB
RefreshInterval: 5 * time.Minute,
OptimizeInterval: 1 * time.Hour,
EnableHighlighting: true,
EnableFaceting: true,
BatchSize: 100,
MaxResults: 1000,
}
}
// NewIndexManager creates a new index manager
func NewIndexManager(basePath, nodeID string, options *IndexManagerOptions) (*IndexManagerImpl, error) {
if options == nil {
options = DefaultIndexManagerOptions()
}
im := &IndexManagerImpl{
indexes: make(map[string]bleve.Index),
stats: make(map[string]*IndexStatistics),
basePath: basePath,
nodeID: nodeID,
options: options,
}
// Start background optimization if enabled
if options.OptimizeInterval > 0 {
go im.optimizationLoop()
}
return im, nil
}
// CreateIndex creates a search index for contexts
func (im *IndexManagerImpl) CreateIndex(
ctx context.Context,
indexName string,
config *IndexConfig,
) error {
im.mu.Lock()
defer im.mu.Unlock()
// Check if index already exists
if _, exists := im.indexes[indexName]; exists {
return fmt.Errorf("index %s already exists", indexName)
}
// Create index mapping
mapping, err := im.createIndexMapping(config)
if err != nil {
return fmt.Errorf("failed to create index mapping: %w", err)
}
// Create the index
indexPath := fmt.Sprintf("%s/%s.bleve", im.basePath, indexName)
index, err := bleve.New(indexPath, mapping)
if err != nil {
return fmt.Errorf("failed to create index: %w", err)
}
// Store the index
im.indexes[indexName] = index
im.stats[indexName] = &IndexStatistics{
Name: indexName,
LastUpdate: time.Now(),
LastOptimization: time.Now(),
}
return nil
}
// UpdateIndex updates search index with new data
func (im *IndexManagerImpl) UpdateIndex(
ctx context.Context,
indexName string,
key string,
data interface{},
) error {
im.mu.RLock()
index, exists := im.indexes[indexName]
stats := im.stats[indexName]
im.mu.RUnlock()
if !exists {
return fmt.Errorf("index %s does not exist", indexName)
}
// Create indexable document from context data
doc, err := im.createIndexDocument(data)
if err != nil {
return fmt.Errorf("failed to create index document: %w", err)
}
// Check document size
docSize := im.estimateDocumentSize(doc)
if docSize > im.options.MaxDocumentSize {
return fmt.Errorf("document too large: %d bytes exceeds limit of %d", docSize, im.options.MaxDocumentSize)
}
// Index the document
start := time.Now()
if err := index.Index(key, doc); err != nil {
return fmt.Errorf("failed to index document: %w", err)
}
// Update statistics
im.mu.Lock()
stats.DocumentCount++
stats.LastUpdate = time.Now()
stats.IndexSize += docSize
updateTime := time.Since(start)
// Update average indexing time
if stats.AverageQueryTime == 0 {
stats.AverageQueryTime = updateTime
} else {
stats.AverageQueryTime = time.Duration(
float64(stats.AverageQueryTime)*0.9 + float64(updateTime)*0.1,
)
}
im.mu.Unlock()
return nil
}
// DeleteFromIndex removes data from search index
func (im *IndexManagerImpl) DeleteFromIndex(
ctx context.Context,
indexName string,
key string,
) error {
im.mu.RLock()
index, exists := im.indexes[indexName]
stats := im.stats[indexName]
im.mu.RUnlock()
if !exists {
return fmt.Errorf("index %s does not exist", indexName)
}
// Delete the document
if err := index.Delete(key); err != nil {
return fmt.Errorf("failed to delete document: %w", err)
}
// Update statistics
im.mu.Lock()
if stats.DocumentCount > 0 {
stats.DocumentCount--
}
stats.LastUpdate = time.Now()
im.mu.Unlock()
return nil
}
// Search searches indexed data using query
func (im *IndexManagerImpl) Search(
ctx context.Context,
indexName string,
query *SearchQuery,
) (*SearchResults, error) {
start := time.Now()
defer func() {
im.updateSearchStats(indexName, time.Since(start))
}()
im.mu.RLock()
index, exists := im.indexes[indexName]
im.mu.RUnlock()
if !exists {
return nil, fmt.Errorf("index %s does not exist", indexName)
}
// Build search request
searchRequest, err := im.buildSearchRequest(query)
if err != nil {
return nil, fmt.Errorf("failed to build search request: %w", err)
}
// Execute search
searchResult, err := index.Search(searchRequest)
if err != nil {
return nil, fmt.Errorf("search failed: %w", err)
}
// Convert to our search results format
results, err := im.convertSearchResults(searchResult, query)
if err != nil {
return nil, fmt.Errorf("failed to convert search results: %w", err)
}
return results, nil
}
// RebuildIndex rebuilds search index from stored data
func (im *IndexManagerImpl) RebuildIndex(
ctx context.Context,
indexName string,
) error {
im.mu.Lock()
defer im.mu.Unlock()
index, exists := im.indexes[indexName]
if !exists {
return fmt.Errorf("index %s does not exist", indexName)
}
// Close current index
if err := index.Close(); err != nil {
return fmt.Errorf("failed to close index: %w", err)
}
// Delete index files
indexPath := fmt.Sprintf("%s/%s.bleve", im.basePath, indexName)
// Note: In production, you'd want to use proper file system operations
// to delete the index directory
// Recreate index with same configuration
// This is a simplified implementation - in practice you'd need to
// recreate with the original configuration and re-index all documents
mapping := bleve.NewIndexMapping()
newIndex, err := bleve.New(indexPath, mapping)
if err != nil {
return fmt.Errorf("failed to recreate index: %w", err)
}
// Replace in memory
im.indexes[indexName] = newIndex
im.stats[indexName].DocumentCount = 0
im.stats[indexName].LastUpdate = time.Now()
im.stats[indexName].LastOptimization = time.Now()
return nil
}
// OptimizeIndex optimizes search index for performance
func (im *IndexManagerImpl) OptimizeIndex(
ctx context.Context,
indexName string,
) error {
im.mu.RLock()
index, exists := im.indexes[indexName]
stats := im.stats[indexName]
im.mu.RUnlock()
if !exists {
return fmt.Errorf("index %s does not exist", indexName)
}
// Bleve doesn't have explicit optimization, but we can force a merge
// This is a no-op for Bleve, but we update stats
im.mu.Lock()
stats.LastOptimization = time.Now()
stats.FragmentationRatio = im.calculateFragmentationRatio(index)
im.mu.Unlock()
return nil
}
// GetIndexStats returns index statistics
func (im *IndexManagerImpl) GetIndexStats(
ctx context.Context,
indexName string,
) (*IndexStatistics, error) {
im.mu.RLock()
stats, exists := im.stats[indexName]
im.mu.RUnlock()
if !exists {
return nil, fmt.Errorf("index %s does not exist", indexName)
}
// Return a copy
statsCopy := *stats
return &statsCopy, nil
}
// ListIndexes lists all available indexes
func (im *IndexManagerImpl) ListIndexes(ctx context.Context) ([]string, error) {
im.mu.RLock()
defer im.mu.RUnlock()
var indexNames []string
for name := range im.indexes {
indexNames = append(indexNames, name)
}
sort.Strings(indexNames)
return indexNames, nil
}
// Helper methods
func (im *IndexManagerImpl) createIndexMapping(config *IndexConfig) (mapping.IndexMapping, error) {
// Create a new index mapping
indexMapping := bleve.NewIndexMapping()
// Configure default analyzer
analyzer := config.Analyzer
if analyzer == "" {
analyzer = im.options.DefaultAnalyzer
}
// Set document mapping
docMapping := bleve.NewDocumentMapping()
// Map context fields
for _, field := range config.Fields {
fieldMapping := bleve.NewTextFieldMapping()
fieldMapping.Analyzer = analyzer
fieldMapping.Store = true
fieldMapping.Index = true
if im.options.EnableHighlighting {
fieldMapping.IncludeTermVectors = true
}
docMapping.AddFieldMappingsAt(field, fieldMapping)
}
// Add special fields for faceting if enabled
if im.options.EnableFaceting {
// Add tags as keyword field for faceting
tagsMapping := bleve.NewKeywordFieldMapping()
tagsMapping.Store = true
tagsMapping.Index = true
docMapping.AddFieldMappingsAt("tags_facet", tagsMapping)
// Add technologies as keyword field for faceting
techMapping := bleve.NewKeywordFieldMapping()
techMapping.Store = true
techMapping.Index = true
docMapping.AddFieldMappingsAt("technologies_facet", techMapping)
}
// Set default document type
indexMapping.DefaultMapping = docMapping
// Configure analyzers
if config.Language == "en" {
indexMapping.DefaultAnalyzer = en.AnalyzerName
} else {
indexMapping.DefaultAnalyzer = standard.Name
}
return indexMapping, nil
}
func (im *IndexManagerImpl) createIndexDocument(data interface{}) (map[string]interface{}, error) {
// Convert context node to indexable document
contextNode, ok := data.(*slurpContext.ContextNode)
if !ok {
return nil, fmt.Errorf("unsupported data type for indexing")
}
doc := map[string]interface{}{
"path": contextNode.Path,
"ucxl_address": contextNode.UCXLAddress.String(),
"summary": contextNode.Summary,
"purpose": contextNode.Purpose,
"technologies": strings.Join(contextNode.Technologies, " "),
"tags": strings.Join(contextNode.Tags, " "),
"insights": strings.Join(contextNode.Insights, " "),
"overrides_parent": contextNode.OverridesParent,
"context_specificity": contextNode.ContextSpecificity,
"applies_to_children": contextNode.AppliesToChildren,
"rag_confidence": contextNode.RAGConfidence,
"generated_at": contextNode.GeneratedAt,
}
// Add faceting fields if enabled
if im.options.EnableFaceting {
doc["tags_facet"] = contextNode.Tags
doc["technologies_facet"] = contextNode.Technologies
}
// Create searchable content by combining key fields
combinedContent := fmt.Sprintf("%s %s %s %s",
contextNode.Summary,
contextNode.Purpose,
strings.Join(contextNode.Technologies, " "),
strings.Join(contextNode.Insights, " "),
)
doc["content"] = combinedContent
return doc, nil
}
func (im *IndexManagerImpl) buildSearchRequest(query *SearchQuery) (*bleve.SearchRequest, error) {
// Build Bleve search request from our search query
var bleveQuery bleve.Query
if query.Query == "" {
// Match all query
bleveQuery = bleve.NewMatchAllQuery()
} else {
// Text search query
if query.FuzzyMatch {
// Use fuzzy query
bleveQuery = bleve.NewFuzzyQuery(query.Query)
} else {
// Use match query for better scoring
bleveQuery = bleve.NewMatchQuery(query.Query)
}
}
// Add filters
var conjuncts []bleve.Query
conjuncts = append(conjuncts, bleveQuery)
// Technology filters
if len(query.Technologies) > 0 {
for _, tech := range query.Technologies {
techQuery := bleve.NewTermQuery(tech)
techQuery.SetField("technologies_facet")
conjuncts = append(conjuncts, techQuery)
}
}
// Tag filters
if len(query.Tags) > 0 {
for _, tag := range query.Tags {
tagQuery := bleve.NewTermQuery(tag)
tagQuery.SetField("tags_facet")
conjuncts = append(conjuncts, tagQuery)
}
}
// Combine all queries
if len(conjuncts) > 1 {
bleveQuery = bleve.NewConjunctionQuery(conjuncts...)
}
// Create search request
searchRequest := bleve.NewSearchRequest(bleveQuery)
// Set result options
if query.Limit > 0 && query.Limit <= im.options.MaxResults {
searchRequest.Size = query.Limit
} else {
searchRequest.Size = im.options.MaxResults
}
if query.Offset > 0 {
searchRequest.From = query.Offset
}
// Enable highlighting if requested
if query.HighlightTerms && im.options.EnableHighlighting {
searchRequest.Highlight = bleve.NewHighlight()
searchRequest.Highlight.AddField("content")
searchRequest.Highlight.AddField("summary")
searchRequest.Highlight.AddField("purpose")
}
// Add facets if requested
if len(query.Facets) > 0 && im.options.EnableFaceting {
searchRequest.Facets = make(bleve.FacetsRequest)
for _, facet := range query.Facets {
switch facet {
case "technologies":
searchRequest.Facets["technologies"] = bleve.NewFacetRequest("technologies_facet", 10)
case "tags":
searchRequest.Facets["tags"] = bleve.NewFacetRequest("tags_facet", 10)
}
}
}
// Set fields to return
searchRequest.Fields = []string{"*"}
return searchRequest, nil
}
func (im *IndexManagerImpl) convertSearchResults(
searchResult *bleve.SearchResult,
query *SearchQuery,
) (*SearchResults, error) {
results := &SearchResults{
Query: query,
Results: make([]*SearchResult, 0, len(searchResult.Hits)),
TotalResults: int64(searchResult.Total),
ProcessingTime: searchResult.Took,
ProcessedAt: time.Now(),
}
// Convert hits
for i, hit := range searchResult.Hits {
searchHit := &SearchResult{
MatchScore: hit.Score,
MatchedFields: make([]string, 0),
Highlights: make(map[string][]string),
Rank: i + 1,
}
// Extract matched fields from hit
for field := range hit.Fields {
searchHit.MatchedFields = append(searchHit.MatchedFields, field)
}
// Extract highlights
for field, fragments := range hit.Fragments {
searchHit.Highlights[field] = fragments
}
// Create context node from hit data (simplified)
contextNode := &slurpContext.ContextNode{
Path: hit.Fields["path"].(string),
Summary: hit.Fields["summary"].(string),
Purpose: hit.Fields["purpose"].(string),
}
// Parse UCXL address
if ucxlStr, ok := hit.Fields["ucxl_address"].(string); ok {
if addr, err := ucxl.ParseAddress(ucxlStr); err == nil {
contextNode.UCXLAddress = addr
}
}
searchHit.Context = contextNode
results.Results = append(results.Results, searchHit)
}
// Convert facets
if len(searchResult.Facets) > 0 {
results.Facets = make(map[string]map[string]int)
for facetName, facetResult := range searchResult.Facets {
facetCounts := make(map[string]int)
for _, term := range facetResult.Terms {
facetCounts[term.Term] = term.Count
}
results.Facets[facetName] = facetCounts
}
}
return results, nil
}
func (im *IndexManagerImpl) estimateDocumentSize(doc map[string]interface{}) int64 {
// Rough estimation of document size
docBytes, _ := json.Marshal(doc)
return int64(len(docBytes))
}
func (im *IndexManagerImpl) calculateFragmentationRatio(index bleve.Index) float64 {
// Simplified fragmentation calculation
// In practice, this would analyze the index structure
return 0.1 // Placeholder: 10% fragmentation
}
func (im *IndexManagerImpl) updateSearchStats(indexName string, duration time.Duration) {
im.mu.Lock()
defer im.mu.Unlock()
stats, exists := im.stats[indexName]
if !exists {
return
}
stats.QueryCount++
if stats.AverageQueryTime == 0 {
stats.AverageQueryTime = duration
} else {
// Exponential moving average
stats.AverageQueryTime = time.Duration(
float64(stats.AverageQueryTime)*0.9 + float64(duration)*0.1,
)
}
// Update success rate (simplified - assumes all queries succeed)
stats.SuccessRate = 1.0
}
func (im *IndexManagerImpl) optimizationLoop() {
ticker := time.NewTicker(im.options.OptimizeInterval)
defer ticker.Stop()
for range ticker.C {
im.performOptimization()
}
}
func (im *IndexManagerImpl) performOptimization() {
im.mu.RLock()
indexNames := make([]string, 0, len(im.indexes))
for name := range im.indexes {
indexNames = append(indexNames, name)
}
im.mu.RUnlock()
// Optimize each index
for _, indexName := range indexNames {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
if err := im.OptimizeIndex(ctx, indexName); err != nil {
// Log error but continue with other indexes
fmt.Printf("Failed to optimize index %s: %v\n", indexName, err)
}
cancel()
}
}
// Close closes all indexes and cleans up resources
func (im *IndexManagerImpl) Close() error {
im.mu.Lock()
defer im.mu.Unlock()
for name, index := range im.indexes {
if err := index.Close(); err != nil {
fmt.Printf("Failed to close index %s: %v\n", name, err)
}
}
im.indexes = make(map[string]bleve.Index)
im.stats = make(map[string]*IndexStatistics)
return nil
}