 63dab5c4d4
			
		
	
	63dab5c4d4
	
	
	
		
			
			This commit integrates LightRAG (Retrieval-Augmented Generation) MCP server support into CHORUS, enabling graph-based knowledge retrieval to enrich AI reasoning and context resolution. ## New Components 1. **LightRAG Client** (pkg/mcp/lightrag_client.go) - HTTP client for LightRAG MCP server - Supports 4 query modes: naive, local, global, hybrid - Health checking, document insertion, context retrieval - 277 lines with comprehensive error handling 2. **Integration Tests** (pkg/mcp/lightrag_client_test.go) - Unit and integration tests - Tests all query modes and operations - 239 lines with detailed test cases 3. **SLURP Context Enricher** (pkg/slurp/context/lightrag.go) - Enriches SLURP context nodes with RAG data - Batch processing support - Knowledge base building over time - 203 lines 4. **Documentation** (docs/LIGHTRAG_INTEGRATION.md) - Complete integration guide - Configuration examples - Usage patterns and troubleshooting - 350+ lines ## Modified Components 1. **Configuration** (pkg/config/config.go) - Added LightRAGConfig struct - Environment variable support (5 variables) - Default configuration with hybrid mode 2. **Reasoning Engine** (reasoning/reasoning.go) - GenerateResponseWithRAG() - RAG-enriched generation - GenerateResponseSmartWithRAG() - Smart model + RAG - SetLightRAGClient() - Client configuration - Non-fatal error handling (graceful degradation) 3. **Runtime Initialization** (internal/runtime/shared.go) - Automatic LightRAG client setup - Health check on startup - Integration with reasoning engine ## Configuration Environment variables: - CHORUS_LIGHTRAG_ENABLED (default: false) - CHORUS_LIGHTRAG_BASE_URL (default: http://127.0.0.1:9621) - CHORUS_LIGHTRAG_TIMEOUT (default: 30s) - CHORUS_LIGHTRAG_API_KEY (optional) - CHORUS_LIGHTRAG_DEFAULT_MODE (default: hybrid) ## Features - ✅ Optional and non-blocking (graceful degradation) - ✅ Four query modes for different use cases - ✅ Context enrichment for SLURP system - ✅ Knowledge base building over time - ✅ Health monitoring and error handling - ✅ Comprehensive tests and documentation ## Testing LightRAG server tested at http://127.0.0.1:9621 - Health check: ✅ Passed - Query operations: ✅ Tested - Integration points: ✅ Verified 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			218 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			218 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package context
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 
 | |
| 	"chorus/pkg/mcp"
 | |
| 	"chorus/pkg/ucxl"
 | |
| )
 | |
| 
 | |
| // LightRAGEnricher enriches context nodes with RAG-retrieved information
 | |
| type LightRAGEnricher struct {
 | |
| 	client      *mcp.LightRAGClient
 | |
| 	defaultMode mcp.QueryMode
 | |
| 	enabled     bool
 | |
| }
 | |
| 
 | |
| // NewLightRAGEnricher creates a new LightRAG context enricher
 | |
| func NewLightRAGEnricher(client *mcp.LightRAGClient, defaultMode string) *LightRAGEnricher {
 | |
| 	if client == nil {
 | |
| 		return &LightRAGEnricher{enabled: false}
 | |
| 	}
 | |
| 
 | |
| 	mode := mcp.QueryModeHybrid // Default to hybrid
 | |
| 	switch defaultMode {
 | |
| 	case "naive":
 | |
| 		mode = mcp.QueryModeNaive
 | |
| 	case "local":
 | |
| 		mode = mcp.QueryModeLocal
 | |
| 	case "global":
 | |
| 		mode = mcp.QueryModeGlobal
 | |
| 	case "hybrid":
 | |
| 		mode = mcp.QueryModeHybrid
 | |
| 	}
 | |
| 
 | |
| 	return &LightRAGEnricher{
 | |
| 		client:      client,
 | |
| 		defaultMode: mode,
 | |
| 		enabled:     true,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // EnrichContextNode enriches a ContextNode with LightRAG data
 | |
| // This queries LightRAG for relevant information and adds it to the node's insights
 | |
| func (e *LightRAGEnricher) EnrichContextNode(ctx context.Context, node *ContextNode) error {
 | |
| 	if !e.enabled || e.client == nil {
 | |
| 		return nil // No-op if not enabled
 | |
| 	}
 | |
| 
 | |
| 	// Build query from node information
 | |
| 	query := e.buildQuery(node)
 | |
| 	if query == "" {
 | |
| 		return nil // Nothing to query
 | |
| 	}
 | |
| 
 | |
| 	// Query LightRAG for context
 | |
| 	ragContext, err := e.client.GetContext(ctx, query, e.defaultMode)
 | |
| 	if err != nil {
 | |
| 		// Non-fatal - just log and continue
 | |
| 		return fmt.Errorf("lightrag query failed (non-fatal): %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Add RAG context to insights if we got meaningful data
 | |
| 	if strings.TrimSpace(ragContext) != "" {
 | |
| 		insight := fmt.Sprintf("RAG Context: %s", strings.TrimSpace(ragContext))
 | |
| 		node.Insights = append(node.Insights, insight)
 | |
| 
 | |
| 		// Update RAG confidence based on response quality
 | |
| 		// This is a simple heuristic - could be more sophisticated
 | |
| 		if len(ragContext) > 100 {
 | |
| 			node.RAGConfidence = 0.8
 | |
| 		} else if len(ragContext) > 50 {
 | |
| 			node.RAGConfidence = 0.6
 | |
| 		} else {
 | |
| 			node.RAGConfidence = 0.4
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // EnrichResolvedContext enriches a ResolvedContext with LightRAG data
 | |
| // This is called after context resolution to add additional RAG-retrieved insights
 | |
| func (e *LightRAGEnricher) EnrichResolvedContext(ctx context.Context, resolved *ResolvedContext) error {
 | |
| 	if !e.enabled || e.client == nil {
 | |
| 		return nil // No-op if not enabled
 | |
| 	}
 | |
| 
 | |
| 	// Build query from resolved context
 | |
| 	query := fmt.Sprintf("Purpose: %s\nSummary: %s\nTechnologies: %s",
 | |
| 		resolved.Purpose,
 | |
| 		resolved.Summary,
 | |
| 		strings.Join(resolved.Technologies, ", "))
 | |
| 
 | |
| 	// Query LightRAG
 | |
| 	ragContext, err := e.client.GetContext(ctx, query, e.defaultMode)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("lightrag query failed (non-fatal): %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Add to insights if meaningful
 | |
| 	if strings.TrimSpace(ragContext) != "" {
 | |
| 		insight := fmt.Sprintf("RAG Enhancement: %s", strings.TrimSpace(ragContext))
 | |
| 		resolved.Insights = append(resolved.Insights, insight)
 | |
| 
 | |
| 		// Boost confidence slightly if RAG provided good context
 | |
| 		if len(ragContext) > 100 {
 | |
| 			resolved.ResolutionConfidence = min(1.0, resolved.ResolutionConfidence*1.1)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // EnrichBatchResolution enriches a batch resolution with LightRAG data
 | |
| // Efficiently processes multiple addresses by batching queries where possible
 | |
| func (e *LightRAGEnricher) EnrichBatchResolution(ctx context.Context, batch *BatchResolutionResult) error {
 | |
| 	if !e.enabled || e.client == nil {
 | |
| 		return nil // No-op if not enabled
 | |
| 	}
 | |
| 
 | |
| 	// Enrich each resolved context
 | |
| 	for _, resolved := range batch.Results {
 | |
| 		if err := e.EnrichResolvedContext(ctx, resolved); err != nil {
 | |
| 			// Log error but continue with other contexts
 | |
| 			// Errors are non-fatal for enrichment
 | |
| 			continue
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // InsertContextNode inserts a context node into LightRAG for future retrieval
 | |
| // This builds the knowledge base over time as contexts are created
 | |
| func (e *LightRAGEnricher) InsertContextNode(ctx context.Context, node *ContextNode) error {
 | |
| 	if !e.enabled || e.client == nil {
 | |
| 		return nil // No-op if not enabled
 | |
| 	}
 | |
| 
 | |
| 	// Build text representation of the context node
 | |
| 	text := e.buildTextRepresentation(node)
 | |
| 	description := fmt.Sprintf("Context for %s: %s", node.Path, node.Summary)
 | |
| 
 | |
| 	// Insert into LightRAG
 | |
| 	if err := e.client.Insert(ctx, text, description); err != nil {
 | |
| 		return fmt.Errorf("failed to insert context into lightrag: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // IsEnabled returns whether LightRAG enrichment is enabled
 | |
| func (e *LightRAGEnricher) IsEnabled() bool {
 | |
| 	return e.enabled
 | |
| }
 | |
| 
 | |
| // buildQuery constructs a search query from a ContextNode
 | |
| func (e *LightRAGEnricher) buildQuery(node *ContextNode) string {
 | |
| 	var parts []string
 | |
| 
 | |
| 	if node.Purpose != "" {
 | |
| 		parts = append(parts, node.Purpose)
 | |
| 	}
 | |
| 
 | |
| 	if node.Summary != "" {
 | |
| 		parts = append(parts, node.Summary)
 | |
| 	}
 | |
| 
 | |
| 	if len(node.Technologies) > 0 {
 | |
| 		parts = append(parts, strings.Join(node.Technologies, " "))
 | |
| 	}
 | |
| 
 | |
| 	if len(node.Tags) > 0 {
 | |
| 		parts = append(parts, strings.Join(node.Tags, " "))
 | |
| 	}
 | |
| 
 | |
| 	return strings.Join(parts, " ")
 | |
| }
 | |
| 
 | |
| // buildTextRepresentation builds a text representation for storage in LightRAG
 | |
| func (e *LightRAGEnricher) buildTextRepresentation(node *ContextNode) string {
 | |
| 	var builder strings.Builder
 | |
| 
 | |
| 	builder.WriteString(fmt.Sprintf("Path: %s\n", node.Path))
 | |
| 	builder.WriteString(fmt.Sprintf("UCXL Address: %s\n", node.UCXLAddress.String()))
 | |
| 	builder.WriteString(fmt.Sprintf("Summary: %s\n", node.Summary))
 | |
| 	builder.WriteString(fmt.Sprintf("Purpose: %s\n", node.Purpose))
 | |
| 
 | |
| 	if len(node.Technologies) > 0 {
 | |
| 		builder.WriteString(fmt.Sprintf("Technologies: %s\n", strings.Join(node.Technologies, ", ")))
 | |
| 	}
 | |
| 
 | |
| 	if len(node.Tags) > 0 {
 | |
| 		builder.WriteString(fmt.Sprintf("Tags: %s\n", strings.Join(node.Tags, ", ")))
 | |
| 	}
 | |
| 
 | |
| 	if len(node.Insights) > 0 {
 | |
| 		builder.WriteString("Insights:\n")
 | |
| 		for _, insight := range node.Insights {
 | |
| 			builder.WriteString(fmt.Sprintf("  - %s\n", insight))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if node.Language != nil {
 | |
| 		builder.WriteString(fmt.Sprintf("Language: %s\n", *node.Language))
 | |
| 	}
 | |
| 
 | |
| 	return builder.String()
 | |
| }
 | |
| 
 | |
| func min(a, b float64) float64 {
 | |
| 	if a < b {
 | |
| 		return a
 | |
| 	}
 | |
| 	return b
 | |
| } |