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>
265 lines
7.3 KiB
Go
265 lines
7.3 KiB
Go
package mcp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// LightRAGClient provides access to LightRAG MCP server
|
|
type LightRAGClient struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
apiKey string // Optional API key for authentication
|
|
}
|
|
|
|
// LightRAGConfig holds configuration for LightRAG client
|
|
type LightRAGConfig struct {
|
|
BaseURL string // e.g., "http://127.0.0.1:9621"
|
|
Timeout time.Duration // HTTP timeout
|
|
APIKey string // Optional API key
|
|
}
|
|
|
|
// QueryMode represents LightRAG query modes
|
|
type QueryMode string
|
|
|
|
const (
|
|
QueryModeNaive QueryMode = "naive" // Simple semantic search
|
|
QueryModeLocal QueryMode = "local" // Local graph traversal
|
|
QueryModeGlobal QueryMode = "global" // Global graph analysis
|
|
QueryModeHybrid QueryMode = "hybrid" // Combined approach
|
|
)
|
|
|
|
// QueryRequest represents a LightRAG query request
|
|
type QueryRequest struct {
|
|
Query string `json:"query"`
|
|
Mode QueryMode `json:"mode"`
|
|
OnlyNeedContext bool `json:"only_need_context,omitempty"`
|
|
}
|
|
|
|
// QueryResponse represents a LightRAG query response
|
|
type QueryResponse struct {
|
|
Response string `json:"response"`
|
|
Context string `json:"context,omitempty"`
|
|
}
|
|
|
|
// InsertRequest represents a LightRAG document insertion request
|
|
type InsertRequest struct {
|
|
Text string `json:"text"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
// InsertResponse represents a LightRAG insertion response
|
|
type InsertResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// HealthResponse represents LightRAG health check response
|
|
type HealthResponse struct {
|
|
Status string `json:"status"`
|
|
WorkingDirectory string `json:"working_directory"`
|
|
InputDirectory string `json:"input_directory"`
|
|
Configuration map[string]interface{} `json:"configuration"`
|
|
AuthMode string `json:"auth_mode"`
|
|
PipelineBusy bool `json:"pipeline_busy"`
|
|
KeyedLocks map[string]interface{} `json:"keyed_locks"`
|
|
CoreVersion string `json:"core_version"`
|
|
APIVersion string `json:"api_version"`
|
|
WebUITitle string `json:"webui_title"`
|
|
WebUIDescription string `json:"webui_description"`
|
|
}
|
|
|
|
// NewLightRAGClient creates a new LightRAG MCP client
|
|
func NewLightRAGClient(config LightRAGConfig) *LightRAGClient {
|
|
if config.Timeout == 0 {
|
|
config.Timeout = 30 * time.Second
|
|
}
|
|
|
|
return &LightRAGClient{
|
|
baseURL: config.BaseURL,
|
|
httpClient: &http.Client{
|
|
Timeout: config.Timeout,
|
|
},
|
|
apiKey: config.APIKey,
|
|
}
|
|
}
|
|
|
|
// Query performs a RAG query against LightRAG
|
|
func (c *LightRAGClient) Query(ctx context.Context, query string, mode QueryMode) (*QueryResponse, error) {
|
|
req := QueryRequest{
|
|
Query: query,
|
|
Mode: mode,
|
|
}
|
|
|
|
respData, err := c.post(ctx, "/query", req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query failed: %w", err)
|
|
}
|
|
|
|
var response QueryResponse
|
|
if err := json.Unmarshal(respData, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
return &response, nil
|
|
}
|
|
|
|
// QueryWithContext performs a RAG query and returns both response and context
|
|
func (c *LightRAGClient) QueryWithContext(ctx context.Context, query string, mode QueryMode) (*QueryResponse, error) {
|
|
req := QueryRequest{
|
|
Query: query,
|
|
Mode: mode,
|
|
OnlyNeedContext: false, // Get both response and context
|
|
}
|
|
|
|
respData, err := c.post(ctx, "/query", req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query with context failed: %w", err)
|
|
}
|
|
|
|
var response QueryResponse
|
|
if err := json.Unmarshal(respData, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
return &response, nil
|
|
}
|
|
|
|
// GetContext retrieves context without generating a response
|
|
func (c *LightRAGClient) GetContext(ctx context.Context, query string, mode QueryMode) (string, error) {
|
|
req := QueryRequest{
|
|
Query: query,
|
|
Mode: mode,
|
|
OnlyNeedContext: true,
|
|
}
|
|
|
|
respData, err := c.post(ctx, "/query", req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("get context failed: %w", err)
|
|
}
|
|
|
|
var response QueryResponse
|
|
if err := json.Unmarshal(respData, &response); err != nil {
|
|
return "", fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
return response.Context, nil
|
|
}
|
|
|
|
// Insert adds a document to the LightRAG knowledge base
|
|
func (c *LightRAGClient) Insert(ctx context.Context, text, description string) error {
|
|
req := InsertRequest{
|
|
Text: text,
|
|
Description: description,
|
|
}
|
|
|
|
respData, err := c.post(ctx, "/insert", req)
|
|
if err != nil {
|
|
return fmt.Errorf("insert failed: %w", err)
|
|
}
|
|
|
|
var response InsertResponse
|
|
if err := json.Unmarshal(respData, &response); err != nil {
|
|
return fmt.Errorf("failed to parse insert response: %w", err)
|
|
}
|
|
|
|
if !response.Success {
|
|
return fmt.Errorf("insert failed: %s", response.Message)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Health checks the health of the LightRAG server
|
|
func (c *LightRAGClient) Health(ctx context.Context) (*HealthResponse, error) {
|
|
respData, err := c.get(ctx, "/health")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("health check failed: %w", err)
|
|
}
|
|
|
|
var response HealthResponse
|
|
if err := json.Unmarshal(respData, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to parse health response: %w", err)
|
|
}
|
|
|
|
return &response, nil
|
|
}
|
|
|
|
// IsHealthy checks if LightRAG server is healthy
|
|
func (c *LightRAGClient) IsHealthy(ctx context.Context) bool {
|
|
health, err := c.Health(ctx)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return health.Status == "healthy"
|
|
}
|
|
|
|
// post performs an HTTP POST request
|
|
func (c *LightRAGClient) post(ctx context.Context, endpoint string, body interface{}) ([]byte, error) {
|
|
jsonData, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+endpoint, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if c.apiKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respData, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(respData))
|
|
}
|
|
|
|
return respData, nil
|
|
}
|
|
|
|
// get performs an HTTP GET request
|
|
func (c *LightRAGClient) get(ctx context.Context, endpoint string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+endpoint, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
if c.apiKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respData, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, string(respData))
|
|
}
|
|
|
|
return respData, nil
|
|
} |