Skip to content

Latest commit

 

History

History
executable file
·
1738 lines (1450 loc) · 51.4 KB

File metadata and controls

executable file
·
1738 lines (1450 loc) · 51.4 KB

Detailed Implementation Guide - Phase 1

Foundation & Core Fixes (Week 1-2)

Overview

This guide provides step-by-step instructions for implementing Phase 1 of the project completion plan, focusing on fixing compilation errors and implementing missing core services.


Week 1: Core Service Implementation

Day 1: Implement Debate Service Types

Step 1.1: Create Debate Types File

File: internal/services/debate_types.go

package services

import (
    "time"
)

// DebateResult represents the outcome of a complete AI debate
type DebateResult struct {
    DebateID          string                `json:"debate_id"`
    StartTime         time.Time             `json:"start_time"`
    EndTime           time.Time             `json:"end_time"`
    Duration          time.Duration         `json:"duration"`
    TotalRounds       int                   `json:"total_rounds"`
    Participants      []ParticipantResponse `json:"participants"`
    Consensus         *ConsensusResult      `json:"consensus,omitempty"`
    CogneeInsights    *CogneeInsights       `json:"cognee_insights,omitempty"`
    QualityScore      float64               `json:"quality_score"`
    Success           bool                  `json:"success"`
    ErrorMessage      string                `json:"error_message,omitempty"`
    Metadata          map[string]interface{} `json:"metadata,omitempty"`
}

// ConsensusResult represents the consensus reached during debate
type ConsensusResult struct {
    Achieved          bool                  `json:"achieved"`
    Confidence        float64               `json:"confidence"`
    AgreementLevel    float64               `json:"agreement_level"`
    FinalPosition     string                `json:"final_position"`
    KeyPoints         []string              `json:"key_points"`
    Disagreements     []string              `json:"disagreements"`
    VotingSummary     VotingSummary         `json:"voting_summary"`
    Timestamp         time.Time             `json:"timestamp"`
}

// ParticipantResponse represents a single participant's response
type ParticipantResponse struct {
    ParticipantID     string                `json:"participant_id"`
    ParticipantName   string                `json:"participant_name"`
    Role              string                `json:"role"`
    Round             int                   `json:"round"`
    Response          string                `json:"response"`
    Confidence        float64               `json:"confidence"`
    QualityScore      float64               `json:"quality_score"`
    ResponseTime      time.Duration         `json:"response_time"`
    LLMProvider       string                `json:"llm_provider"`
    LLMModel          string                `json:"llm_model"`
    CogneeEnhanced    bool                  `json:"cognee_enhanced"`
    CogneeAnalysis    *CogneeAnalysis       `json:"cognee_analysis,omitempty"`
    Timestamp         time.Time             `json:"timestamp"`
}

// CogneeInsights represents insights generated by Cognee AI
type CogneeInsights struct {
    DatasetName       string                `json:"dataset_name"`
    EnhancementTime   time.Duration         `json:"enhancement_time"`
    SemanticAnalysis  SemanticAnalysis      `json:"semantic_analysis"`
    EntityExtraction  []Entity              `json:"entity_extraction"`
    SentimentAnalysis SentimentAnalysis     `json:"sentiment_analysis"`
    KnowledgeGraph    KnowledgeGraph        `json:"knowledge_graph"`
    Recommendations   []string              `json:"recommendations"`
    QualityMetrics    QualityMetrics        `json:"quality_metrics"`
}

// CogneeAnalysis represents Cognee analysis for a single response
type CogneeAnalysis struct {
    Enhanced          bool                  `json:"enhanced"`
    OriginalResponse  string                `json:"original_response,omitempty"`
    EnhancedResponse  string                `json:"enhanced_response,omitempty"`
    Sentiment         string                `json:"sentiment"`
    Entities          []string              `json:"entities"`
    KeyPhrases        []string              `json:"key_phrases"`
    Confidence        float64               `json:"confidence"`
    ProcessingTime    time.Duration         `json:"processing_time"`
}

// VotingSummary represents the voting results
type VotingSummary struct {
    Strategy          string                `json:"strategy"`
    TotalVotes        int                   `json:"total_votes"`
    VoteDistribution  map[string]int        `json:"vote_distribution"`
    Winner            string                `json:"winner"`
    Margin            float64               `json:"margin"`
}

// SemanticAnalysis represents semantic analysis results
type SemanticAnalysis struct {
    SimilarityMatrix  [][]float64           `json:"similarity_matrix"`
    Clusters          []Cluster             `json:"clusters"`
    MainThemes        []string              `json:"main_themes"`
    CoherenceScore    float64               `json:"coherence_score"`
}

// SentimentAnalysis represents sentiment analysis results
type SentimentAnalysis struct {
    OverallSentiment  string                `json:"overall_sentiment"`
    SentimentScore    float64               `json:"sentiment_score"`
    SentimentByRound  []SentimentByRound    `json:"sentiment_by_round"`
}

// KnowledgeGraph represents the knowledge graph
type KnowledgeGraph struct {
    Nodes             []Node                `json:"nodes"`
    Edges             []Edge                `json:"edges"`
    CentralConcepts   []string              `json:"central_concepts"`
}

// QualityMetrics represents quality metrics
type QualityMetrics struct {
    Coherence         float64               `json:"coherence"`
    Relevance         float64               `json:"relevance"`
    Accuracy          float64               `json:"accuracy"`
    Completeness      float64               `json:"completeness"`
    OverallScore      float64               `json:"overall_score"`
}

// Helper types
type Entity struct {
    Text              string                `json:"text"`
    Type              string                `json:"type"`
    Confidence        float64               `json:"confidence"`
}

type Cluster struct {
    ID                string                `json:"id"`
    Members           []string              `json:"members"`
    Centroid          string                `json:"centroid"`
}

type SentimentByRound struct {
    Round             int                   `json:"round"`
    Sentiment         string                `json:"sentiment"`
    Score             float64               `json:"score"`
}

type Node struct {
    ID                string                `json:"id"`
    Label             string                `json:"label"`
    Type              string                `json:"type"`
    Properties        map[string]interface{} `json:"properties"`
}

type Edge struct {
    Source            string                `json:"source"`
    Target            string                `json:"target"`
    Type              string                `json:"type"`
    Weight            float64               `json:"weight"`
}

// Validate validates the DebateResult
func (dr *DebateResult) Validate() error {
    if dr.DebateID == "" {
        return fmt.Errorf("debate_id is required")
    }
    if dr.StartTime.IsZero() {
        return fmt.Errorf("start_time is required")
    }
    if dr.TotalRounds < 1 {
        return fmt.Errorf("total_rounds must be at least 1")
    }
    if len(dr.Participants) < 2 {
        return fmt.Errorf("at least 2 participants required")
    }
    return nil
}

// Validate validates the ConsensusResult
func (cr *ConsensusResult) Validate() error {
    if cr.Timestamp.IsZero() {
        return fmt.Errorf("timestamp is required")
    }
    if cr.Confidence < 0 || cr.Confidence > 1 {
        return fmt.Errorf("confidence must be between 0 and 1")
    }
    return nil
}

// Validate validates the ParticipantResponse
func (pr *ParticipantResponse) Validate() error {
    if pr.ParticipantID == "" {
        return fmt.Errorf("participant_id is required")
    }
    if pr.ParticipantName == "" {
        return fmt.Errorf("participant_name is required")
    }
    if pr.Round < 1 {
        return fmt.Errorf("round must be at least 1")
    }
    if pr.Response == "" {
        return fmt.Errorf("response is required")
    }
    if pr.Timestamp.IsZero() {
        return fmt.Errorf("timestamp is required")
    }
    return nil
}

Step 1.2: Update Test Imports

File: tests/e2e/ai_debate_e2e_test.go

Add import for services package at the top:

import (
    "dev.helix.agent/internal/services"
)

Step 1.3: Verify Compilation

go build ./tests/e2e

Day 2: Implement Advanced Debate Services

Step 2.1: Create Advanced Debate Service

File: internal/services/advanced_debate_service.go

package services

import (
    "context"
    "fmt"
    "time"
)

// AdvancedDebateService provides advanced debate management capabilities
type AdvancedDebateService struct {
    debateService      *DebateService
    monitoringService  *DebateMonitoringService
    performanceService  *DebatePerformanceService
    historyService     *DebateHistoryService
    resilienceService  *DebateResilienceService
    reportingService   *DebateReportingService
    securityService    *DebateSecurityService
    logger            *logrus.Logger
}

// NewAdvancedDebateService creates a new advanced debate service
func NewAdvancedDebateService(
    debateService *DebateService,
    monitoringService *DebateMonitoringService,
    performanceService *DebatePerformanceService,
    historyService *DebateHistoryService,
    resilienceService *DebateResilienceService,
    reportingService *DebateReportingService,
    securityService *DebateSecurityService,
    logger *logrus.Logger,
) *AdvancedDebateService {
    return &AdvancedDebateService{
        debateService:     debateService,
        monitoringService: monitoringService,
        performanceService: performanceService,
        historyService:    historyService,
        resilienceService: resilienceService,
        reportingService:  reportingService,
        securityService:   securityService,
        logger:           logger,
    }
}

// ConductAdvancedDebate conducts a debate with advanced features
func (ads *AdvancedDebateService) ConductAdvancedDebate(
    ctx context.Context,
    config *DebateConfig,
) (*DebateResult, error) {
    // Security check
    if err := ads.securityService.ValidateDebateRequest(ctx, config); err != nil {
        return nil, fmt.Errorf("security validation failed: %w", err)
    }

    // Start monitoring
    monitoringID, err := ads.monitoringService.StartMonitoring(ctx, config)
    if err != nil {
        return nil, fmt.Errorf("failed to start monitoring: %w", err)
    }
    defer ads.monitoringService.StopMonitoring(ctx, monitoringID)

    // Conduct debate
    result, err := ads.debateService.ConductDebate(ctx, config)
    if err != nil {
        return nil, fmt.Errorf("debate failed: %w", err)
    }

    // Record performance metrics
    metrics := ads.performanceService.CalculateMetrics(result)
    if err := ads.performanceService.RecordMetrics(ctx, metrics); err != nil {
        ads.logger.Warnf("Failed to record performance metrics: %v", err)
    }

    // Save to history
    if err := ads.historyService.SaveDebateResult(ctx, result); err != nil {
        ads.logger.Warnf("Failed to save debate to history: %v", err)
    }

    // Generate report
    report, err := ads.reportingService.GenerateReport(ctx, result)
    if err != nil {
        ads.logger.Warnf("Failed to generate report: %v", err)
    } else {
        result.Metadata["report"] = report
    }

    return result, nil
}

// GetDebateStatus retrieves the current status of a debate
func (ads *AdvancedDebateService) GetDebateStatus(
    ctx context.Context,
    debateID string,
) (*DebateStatus, error) {
    return ads.monitoringService.GetStatus(ctx, debateID)
}

// GetDebateHistory retrieves historical debate data
func (ads *AdvancedDebateService) GetDebateHistory(
    ctx context.Context,
    filters *HistoryFilters,
) ([]*DebateResult, error) {
    return ads.historyService.QueryHistory(ctx, filters)
}

// GetPerformanceMetrics retrieves performance metrics
func (ads *AdvancedDebateService) GetPerformanceMetrics(
    ctx context.Context,
    timeRange TimeRange,
) (*PerformanceMetrics, error) {
    return ads.performanceService.GetMetrics(ctx, timeRange)
}

Step 2.2: Create Supporting Services

Create stub implementations for all supporting services:

File: internal/services/debate_monitoring_service.go

package services

import (
    "context"
    "time"
)

type DebateMonitoringService struct {
    logger *logrus.Logger
}

func NewDebateMonitoringService(logger *logrus.Logger) *DebateMonitoringService {
    return &DebateMonitoringService{logger: logger}
}

func (dms *DebateMonitoringService) StartMonitoring(ctx context.Context, config *DebateConfig) (string, error) {
    return "monitoring-" + time.Now().Format("20060102150405"), nil
}

func (dms *DebateMonitoringService) StopMonitoring(ctx context.Context, monitoringID string) error {
    return nil
}

func (dms *DebateMonitoringService) GetStatus(ctx context.Context, debateID string) (*DebateStatus, error) {
    return &DebateStatus{
        DebateID: debateID,
        Status:   "completed",
    }, nil
}

File: internal/services/debate_performance_service.go

package services

import (
    "context"
    "time"
)

type DebatePerformanceService struct {
    logger *logrus.Logger
}

func NewDebatePerformanceService(logger *logrus.Logger) *DebatePerformanceService {
    return &DebatePerformanceService{logger: logger}
}

func (dps *DebatePerformanceService) CalculateMetrics(result *DebateResult) *PerformanceMetrics {
    return &PerformanceMetrics{
        Duration:      result.Duration,
        TotalRounds:   result.TotalRounds,
        QualityScore:  result.QualityScore,
    }
}

func (dps *DebatePerformanceService) RecordMetrics(ctx context.Context, metrics *PerformanceMetrics) error {
    return nil
}

func (dps *DebatePerformanceService) GetMetrics(ctx context.Context, timeRange TimeRange) (*PerformanceMetrics, error) {
    return &PerformanceMetrics{}, nil
}

File: internal/services/debate_history_service.go

package services

import (
    "context"
)

type DebateHistoryService struct {
    logger *logrus.Logger
}

func NewDebateHistoryService(logger *logrus.Logger) *DebateHistoryService {
    return &DebateHistoryService{logger: logger}
}

func (dhs *DebateHistoryService) SaveDebateResult(ctx context.Context, result *DebateResult) error {
    return nil
}

func (dhs *DebateHistoryService) QueryHistory(ctx context.Context, filters *HistoryFilters) ([]*DebateResult, error) {
    return []*DebateResult{}, nil
}

File: internal/services/debate_resilience_service.go

package services

import (
    "context"
)

type DebateResilienceService struct {
    logger *logrus.Logger
}

func NewDebateResilienceService(logger *logrus.Logger) *DebateResilienceService {
    return &DebateResilienceService{logger: logger}
}

func (drs *DebateResilienceService) HandleFailure(ctx context.Context, err error) error {
    return nil
}

func (drs *DebateResilienceService) RecoverDebate(ctx context.Context, debateID string) (*DebateResult, error) {
    return nil, nil
}

File: internal/services/debate_reporting_service.go

package services

import (
    "context"
)

type DebateReportingService struct {
    logger *logrus.Logger
}

func NewDebateReportingService(logger *logrus.Logger) *DebateReportingService {
    return &DebateReportingService{logger: logger}
}

func (drs *DebateReportingService) GenerateReport(ctx context.Context, result *DebateResult) (*DebateReport, error) {
    return &DebateReport{}, nil
}

func (drs *DebateReportingService) ExportReport(ctx context.Context, reportID string, format string) ([]byte, error) {
    return nil, nil
}

File: internal/services/debate_security_service.go

package services

import (
    "context"
)

type DebateSecurityService struct {
    logger *logrus.Logger
}

func NewDebateSecurityService(logger *logrus.Logger) *DebateSecurityService {
    return &DebateSecurityService{logger: logger}
}

func (dss *DebateSecurityService) ValidateDebateRequest(ctx context.Context, config *DebateConfig) error {
    return nil
}

func (dss *DebateSecurityService) SanitizeResponse(ctx context.Context, response string) (string, error) {
    return response, nil
}

func (dss *DebateSecurityService) AuditDebate(ctx context.Context, debateID string) error {
    return nil
}

Step 2.3: Create Supporting Types

File: internal/services/debate_support_types.go

package services

import (
    "time"
)

// DebateStatus represents the current status of a debate
type DebateStatus struct {
    DebateID          string                `json:"debate_id"`
    Status            string                `json:"status"`
    CurrentRound      int                   `json:"current_round"`
    TotalRounds       int                   `json:"total_rounds"`
    StartTime         time.Time             `json:"start_time"`
    EstimatedEndTime  time.Time             `json:"estimated_end_time,omitempty"`
    Participants      []ParticipantStatus   `json:"participants"`
    Errors            []string              `json:"errors,omitempty"`
    Metadata          map[string]interface{} `json:"metadata,omitempty"`
}

// ParticipantStatus represents a participant's status
type ParticipantStatus struct {
    ParticipantID     string                `json:"participant_id"`
    ParticipantName   string                `json:"participant_name"`
    Status            string                `json:"status"`
    CurrentResponse   string                `json:"current_response,omitempty"`
    ResponseTime      time.Duration         `json:"response_time,omitempty"`
    Error             string                `json:"error,omitempty"`
}

// PerformanceMetrics represents performance metrics
type PerformanceMetrics struct {
    Duration          time.Duration         `json:"duration"`
    TotalRounds       int                   `json:"total_rounds"`
    QualityScore      float64               `json:"quality_score"`
    Throughput        float64               `json:"throughput"`
    Latency           time.Duration         `json:"latency"`
    ErrorRate         float64               `json:"error_rate"`
    ResourceUsage     ResourceUsage         `json:"resource_usage"`
}

// ResourceUsage represents resource usage metrics
type ResourceUsage struct {
    CPU               float64               `json:"cpu"`
    Memory            uint64                `json:"memory"`
    Network           uint64                `json:"network"`
}

// HistoryFilters represents filters for querying debate history
type HistoryFilters struct {
    StartTime         *time.Time            `json:"start_time,omitempty"`
    EndTime           *time.Time            `json:"end_time,omitempty"`
    ParticipantIDs    []string              `json:"participant_ids,omitempty"`
    MinQualityScore   *float64              `json:"min_quality_score,omitempty"`
    MaxQualityScore   *float64              `json:"max_quality_score,omitempty"`
    Limit             int                   `json:"limit,omitempty"`
    Offset            int                   `json:"offset,omitempty"`
}

// TimeRange represents a time range for metrics
type TimeRange struct {
    StartTime         time.Time             `json:"start_time"`
    EndTime           time.Time             `json:"end_time"`
}

// DebateReport represents a generated debate report
type DebateReport struct {
    ReportID          string                `json:"report_id"`
    DebateID          string                `json:"debate_id"`
    GeneratedAt       time.Time             `json:"generated_at"`
    Summary           string                `json:"summary"`
    KeyFindings       []string              `json:"key_findings"`
    Recommendations   []string              `json:"recommendations"`
    Metrics           PerformanceMetrics    `json:"metrics"`
    Appendices        map[string]interface{} `json:"appendices,omitempty"`
}

// DebateConfig represents the configuration for a debate
type DebateConfig struct {
    DebateID          string                `json:"debate_id"`
    Topic             string                `json:"topic"`
    Participants      []ParticipantConfig  `json:"participants"`
    MaxRounds         int                   `json:"max_rounds"`
    Timeout           time.Duration         `json:"timeout"`
    Strategy          string                `json:"strategy"`
    EnableCognee      bool                  `json:"enable_cognee"`
    Metadata          map[string]interface{} `json:"metadata,omitempty"`
}

// ParticipantConfig represents a participant configuration
type ParticipantConfig struct {
    ParticipantID     string                `json:"participant_id"`
    Name              string                `json:"name"`
    Role              string                `json:"role"`
    LLMProvider       string                `json:"llm_provider"`
    LLMModel          string                `json:"llm_model"`
    MaxRounds         int                   `json:"max_rounds"`
    Timeout           time.Duration         `json:"timeout"`
    Weight            float64               `json:"weight"`
}

Step 2.4: Update Test File

File: tests/integration/ai_debate_advanced_integration_test.go

Update the test setup:

package integration

import (
    "context"
    "testing"
    "time"
    
    "github.com/sirupsen/logrus"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    
    "dev.helix.agent/internal/services"
    "dev.helix.agent/internal/utils"
)

func TestAdvancedDebateService_Integration(t *testing.T) {
    // Create logger
    logger := logrus.New()
    logger.SetLevel(logrus.DebugLevel)
    
    // Create services
    debateService := services.NewDebateService(logger)
    monitoringService := services.NewDebateMonitoringService(logger)
    performanceService := services.NewDebatePerformanceService(logger)
    historyService := services.NewDebateHistoryService(logger)
    resilienceService := services.NewDebateResilienceService(logger)
    reportingService := services.NewDebateReportingService(logger)
    securityService := services.NewDebateSecurityService(logger)
    
    // Create advanced debate service
    advancedService := services.NewAdvancedDebateService(
        debateService,
        monitoringService,
        performanceService,
        historyService,
        resilienceService,
        reportingService,
        securityService,
        logger,
    )
    
    // Test configuration
    config := &services.DebateConfig{
        DebateID:     "test-debate-001",
        Topic:        "Test topic for advanced debate",
        MaxRounds:    3,
        Timeout:      5 * time.Minute,
        Strategy:     "structured",
        EnableCognee: true,
    }
    
    // Conduct debate
    ctx := context.Background()
    result, err := advancedService.ConductAdvancedDebate(ctx, config)
    
    // Assertions
    require.NoError(t, err)
    require.NotNil(t, result)
    assert.Equal(t, config.DebateID, result.DebateID)
    assert.True(t, result.Success)
    assert.Greater(t, result.TotalRounds, 0)
}

Day 3: Fix Provider Interface Issues

Step 3.1: Update LLM Provider Interface

File: internal/llm/provider.go

package llm

import (
    "context"
)

// LLMProvider defines the interface for LLM providers
type LLMProvider interface {
    // Complete generates a completion for the given request
    Complete(ctx context.Context, request *LLMRequest) (*LLMResponse, error)
    
    // StreamComplete generates a streaming completion
    StreamComplete(ctx context.Context, request *LLMRequest) (<-chan LLMStreamChunk, error)
    
    // GetCapabilities returns the provider's capabilities
    GetCapabilities() ProviderCapabilities
    
    // GetModel returns the model name
    GetModel() string
    
    // GetProvider returns the provider name
    GetProvider() string
    
    // Validate validates the provider configuration
    Validate() error
    
    // HealthCheck performs a health check
    HealthCheck(ctx context.Context) error
}

// ProviderCapabilities represents the capabilities of a provider
type ProviderCapabilities struct {
    Streaming         bool                  `json:"streaming"`
    MaxTokens         int                   `json:"max_tokens"`
    SupportsSystem    bool                  `json:"supports_system"`
    SupportsImages    bool                  `json:"supports_images"`
    SupportsTools     bool                  `json:"supports_tools"`
    RateLimitRPS      int                   `json:"rate_limit_rps"`
    Timeout           time.Duration         `json:"timeout"`
    Features          []string              `json:"features"`
}

// LLMRequest represents a completion request
type LLMRequest struct {
    Model            string                `json:"model"`
    Messages         []Message             `json:"messages"`
    MaxTokens        int                   `json:"max_tokens,omitempty"`
    Temperature      float64               `json:"temperature,omitempty"`
    TopP             float64               `json:"top_p,omitempty"`
    StopSequences    []string              `json:"stop_sequences,omitempty"`
    Stream           bool                  `json:"stream,omitempty"`
    Metadata         map[string]interface{} `json:"metadata,omitempty"`
}

// LLMResponse represents a completion response
type LLMResponse struct {
    ID               string                `json:"id"`
    Model            string                `json:"model"`
    Choices          []Choice              `json:"choices"`
    Usage            Usage                 `json:"usage"`
    FinishReason     string                `json:"finish_reason"`
    Metadata         map[string]interface{} `json:"metadata,omitempty"`
}

// LLMStreamChunk represents a streaming chunk
type LLMStreamChunk struct {
    ID               string                `json:"id"`
    Model            string                `json:"model"`
    Choices          []StreamChoice        `json:"choices"`
    FinishReason     *string               `json:"finish_reason,omitempty"`
}

// Message represents a message in the conversation
type Message struct {
    Role             string                `json:"role"`
    Content          string                `json:"content"`
}

// Choice represents a completion choice
type Choice struct {
    Index            int                   `json:"index"`
    Message          Message               `json:"message"`
    FinishReason     string                `json:"finish_reason"`
}

// StreamChoice represents a streaming choice
type StreamChoice struct {
    Index            int                   `json:"index"`
    Delta            MessageDelta          `json:"delta"`
    FinishReason     *string               `json:"finish_reason,omitempty"`
}

// MessageDelta represents a delta in a streaming message
type MessageDelta struct {
    Role             *string               `json:"role,omitempty"`
    Content          *string               `json:"content,omitempty"`
}

// Usage represents token usage
type Usage struct {
    PromptTokens     int                   `json:"prompt_tokens"`
    CompletionTokens int                   `json:"completion_tokens"`
    TotalTokens      int                   `json:"total_tokens"}

Step 3.2: Update Mock Provider

File: tests/unit/services/provider_registry_test.go

package services_test

import (
    "context"
    "testing"
    "time"
    
    "github.com/stretchr/testify/mock"
    
    "dev.helix.agent/internal/llm"
    "dev.helix.agent/internal/models"
)

// mockLLMProvider is a mock implementation of LLMProvider
type mockLLMProvider struct {
    mock.Mock
}

func (m *mockLLMProvider) Complete(ctx context.Context, request *llm.LLMRequest) (*llm.LLMResponse, error) {
    args := m.Called(ctx, request)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*llm.LLMResponse), args.Error(1)
}

func (m *mockLLMProvider) StreamComplete(ctx context.Context, request *llm.LLMRequest) (<-chan llm.LLMStreamChunk, error) {
    args := m.Called(ctx, request)
    return args.Get(0).(<-chan llm.LLMStreamChunk), args.Error(1)
}

func (m *mockLLMProvider) GetCapabilities() llm.ProviderCapabilities {
    args := m.Called()
    return args.Get(0).(llm.ProviderCapabilities)
}

func (m *mockLLMProvider) GetModel() string {
    args := m.Called()
    return args.String(0)
}

func (m *mockLLMProvider) GetProvider() string {
    args := m.Called()
    return args.String(0)
}

func (m *mockLLMProvider) Validate() error {
    args := m.Called()
    return args.Error(0)
}

func (m *mockLLMProvider) HealthCheck(ctx context.Context) error {
    args := m.Called(ctx)
    return args.Error(0)
}

Step 3.3: Verify Compilation

go build ./tests/unit/services
go vet ./tests/unit/services

Week 2: LLM Provider Implementations

Day 1: Implement Claude and DeepSeek Providers

Step 1.1: Create Claude Provider

File: internal/llm/providers/claude.go

package providers

import (
    "context"
    "fmt"
    "time"
    
    "github.com/sirupsen/logrus"
    
    "dev.helix.agent/internal/llm"
)

// ClaudeProvider implements LLMProvider for Anthropic Claude
type ClaudeProvider struct {
    apiKey     string
    baseURL    string
    model      string
    timeout    time.Duration
    maxRetries int
    logger     *logrus.Logger
}

// NewClaudeProvider creates a new Claude provider
func NewClaudeProvider(
    apiKey string,
    baseURL string,
    model string,
    timeout time.Duration,
    maxRetries int,
    logger *logrus.Logger,
) (*ClaudeProvider, error) {
    if apiKey == "" {
        return nil, fmt.Errorf("API key is required")
    }
    if model == "" {
        return nil, fmt.Errorf("model is required")
    }
    
    if baseURL == "" {
        baseURL = "https://api.anthropic.com"
    }
    
    if timeout == 0 {
        timeout = 30 * time.Second
    }
    
    if maxRetries == 0 {
        maxRetries = 3
    }
    
    return &ClaudeProvider{
        apiKey:     apiKey,
        baseURL:    baseURL,
        model:      model,
        timeout:    timeout,
        maxRetries: maxRetries,
        logger:     logger,
    }, nil
}

// Complete generates a completion for the given request
func (cp *ClaudeProvider) Complete(ctx context.Context, request *llm.LLMRequest) (*llm.LLMResponse, error) {
    cp.logger.Debugf("ClaudeProvider.Complete called with model: %s", request.Model)

    // Build API request body
    reqBody := map[string]interface{}{
        "model":      cp.model,
        "max_tokens": request.MaxTokens,
        "messages":   cp.convertMessages(request.Messages),
    }

    // Make API call
    jsonBody, _ := json.Marshal(reqBody)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST", cp.baseURL+"/v1/messages", bytes.NewBuffer(jsonBody))
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("x-api-key", cp.apiKey)
    httpReq.Header.Set("anthropic-version", "2023-06-01")

    resp, err := cp.client.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("API request failed: %w", err)
    }
    defer resp.Body.Close()

    // Parse response
    var apiResp anthropicResponse
    if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
        return nil, fmt.Errorf("failed to decode response: %w", err)
    }

    return cp.convertResponse(&apiResp), nil
}

// StreamComplete generates a streaming completion
func (cp *ClaudeProvider) StreamComplete(ctx context.Context, request *llm.LLMRequest) (<-chan llm.LLMStreamChunk, error) {
    cp.logger.Debugf("ClaudeProvider.StreamComplete called with model: %s", request.Model)

    // Build streaming request
    reqBody := map[string]interface{}{
        "model":      cp.model,
        "max_tokens": request.MaxTokens,
        "messages":   cp.convertMessages(request.Messages),
        "stream":     true,
    }

    jsonBody, _ := json.Marshal(reqBody)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST", cp.baseURL+"/v1/messages", bytes.NewBuffer(jsonBody))
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("x-api-key", cp.apiKey)
    httpReq.Header.Set("anthropic-version", "2023-06-01")
    httpReq.Header.Set("Accept", "text/event-stream")

    resp, err := cp.client.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("stream request failed: %w", err)
    }

    chunkChan := make(chan llm.LLMStreamChunk, 100)

    go func() {
        defer close(chunkChan)
        defer resp.Body.Close()

        scanner := bufio.NewScanner(resp.Body)
        for scanner.Scan() {
            line := scanner.Text()
            if strings.HasPrefix(line, "data: ") {
                data := strings.TrimPrefix(line, "data: ")
                if data == "[DONE]" {
                    break
                }
                chunk := cp.parseStreamChunk(data)
                if chunk != nil {
                    chunkChan <- *chunk
                }
            }
        }
    }()

    return chunkChan, nil
}

// GetCapabilities returns the provider's capabilities
func (cp *ClaudeProvider) GetCapabilities() llm.ProviderCapabilities {
    return llm.ProviderCapabilities{
        Streaming:        true,
        MaxTokens:        200000,
        SupportsSystem:   true,
        SupportsImages:   true,
        SupportsTools:    true,
        RateLimitRPS:     50,
        Timeout:         cp.timeout,
        Features:        []string{"long_context", "vision", "tools"},
    }
}

// GetModel returns the model name
func (cp *ClaudeProvider) GetModel() string {
    return cp.model
}

// GetProvider returns the provider name
func (cp *ClaudeProvider) GetProvider() string {
    return "claude"
}

// Validate validates the provider configuration
func (cp *ClaudeProvider) Validate() error {
    if cp.apiKey == "" {
        return fmt.Errorf("API key is required")
    }
    if cp.model == "" {
        return fmt.Errorf("model is required")
    }
    return nil
}

// HealthCheck performs a health check
func (cp *ClaudeProvider) HealthCheck(ctx context.Context) error {
    // Make a minimal API request to verify connectivity
    httpReq, _ := http.NewRequestWithContext(ctx, "GET", cp.baseURL+"/v1/models", nil)
    httpReq.Header.Set("x-api-key", cp.apiKey)
    httpReq.Header.Set("anthropic-version", "2023-06-01")

    resp, err := cp.client.Do(httpReq)
    if err != nil {
        return fmt.Errorf("health check failed: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return fmt.Errorf("health check returned status %d", resp.StatusCode)
    }
    return nil
}

// Helper function
func stringPtr(s string) *string {
    return &s
}

Step 1.2: Create DeepSeek Provider

File: internal/llm/providers/deepseek.go

package providers

import (
    "context"
    "fmt"
    "time"
    
    "github.com/sirupsen/logrus"
    
    "dev.helix.agent/internal/llm"
)

// DeepSeekProvider implements LLMProvider for DeepSeek
type DeepSeekProvider struct {
    apiKey     string
    baseURL    string
    model      string
    timeout    time.Duration
    maxRetries int
    logger     *logrus.Logger
}

// NewDeepSeekProvider creates a new DeepSeek provider
func NewDeepSeekProvider(
    apiKey string,
    baseURL string,
    model string,
    timeout time.Duration,
    maxRetries int,
    logger *logrus.Logger,
) (*DeepSeekProvider, error) {
    if apiKey == "" {
        return nil, fmt.Errorf("API key is required")
    }
    if model == "" {
        return nil, fmt.Errorf("model is required")
    }
    
    if baseURL == "" {
        baseURL = "https://api.deepseek.com"
    }
    
    if timeout == 0 {
        timeout = 30 * time.Second
    }
    
    if maxRetries == 0 {
        maxRetries = 3
    }
    
    return &DeepSeekProvider{
        apiKey:     apiKey,
        baseURL:    baseURL,
        model:      model,
        timeout:    timeout,
        maxRetries: maxRetries,
        logger:     logger,
    }, nil
}

// Complete generates a completion for the given request
func (dsp *DeepSeekProvider) Complete(ctx context.Context, request *llm.LLMRequest) (*llm.LLMResponse, error) {
    dsp.logger.Debugf("DeepSeekProvider.Complete called with model: %s", request.Model)

    // DeepSeek uses OpenAI-compatible API format
    reqBody := map[string]interface{}{
        "model":      dsp.model,
        "messages":   dsp.convertMessages(request.Messages),
        "max_tokens": request.MaxTokens,
    }

    jsonBody, _ := json.Marshal(reqBody)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST", dsp.baseURL+"/v1/chat/completions", bytes.NewBuffer(jsonBody))
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("Authorization", "Bearer "+dsp.apiKey)

    resp, err := dsp.client.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("API request failed: %w", err)
    }
    defer resp.Body.Close()

    var apiResp openAIResponse
    if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
        return nil, fmt.Errorf("failed to decode response: %w", err)
    }

    return dsp.convertResponse(&apiResp), nil
}

// StreamComplete generates a streaming completion
func (dsp *DeepSeekProvider) StreamComplete(ctx context.Context, request *llm.LLMRequest) (<-chan llm.LLMStreamChunk, error) {
    dsp.logger.Debugf("DeepSeekProvider.StreamComplete called with model: %s", request.Model)

    reqBody := map[string]interface{}{
        "model":      dsp.model,
        "messages":   dsp.convertMessages(request.Messages),
        "max_tokens": request.MaxTokens,
        "stream":     true,
    }

    jsonBody, _ := json.Marshal(reqBody)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST", dsp.baseURL+"/v1/chat/completions", bytes.NewBuffer(jsonBody))
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("Authorization", "Bearer "+dsp.apiKey)
    httpReq.Header.Set("Accept", "text/event-stream")

    resp, err := dsp.client.Do(httpReq)
    if err != nil {
        return nil, fmt.Errorf("stream request failed: %w", err)
    }

    chunkChan := make(chan llm.LLMStreamChunk, 100)

    go func() {
        defer close(chunkChan)
        defer resp.Body.Close()

        scanner := bufio.NewScanner(resp.Body)
        for scanner.Scan() {
            line := scanner.Text()
            if strings.HasPrefix(line, "data: ") {
                data := strings.TrimPrefix(line, "data: ")
                if data == "[DONE]" {
                    break
                }
                chunk := dsp.parseStreamChunk(data)
                if chunk != nil {
                    chunkChan <- *chunk
                }
            }
        }
    }()

    return chunkChan, nil
}

// GetCapabilities returns the provider's capabilities
func (dsp *DeepSeekProvider) GetCapabilities() llm.ProviderCapabilities {
    return llm.ProviderCapabilities{
        Streaming:        true,
        MaxTokens:        128000,
        SupportsSystem:   true,
        SupportsImages:   false,
        SupportsTools:    true,
        RateLimitRPS:     100,
        Timeout:         dsp.timeout,
        Features:        []string{"long_context", "tools"},
    }
}

// GetModel returns the model name
func (dsp *DeepSeekProvider) GetModel() string {
    return dsp.model
}

// GetProvider returns the provider name
func (dsp *DeepSeekProvider) GetProvider() string {
    return "deepseek"
}

// Validate validates the provider configuration
func (dsp *DeepSeekProvider) Validate() error {
    if dsp.apiKey == "" {
        return fmt.Errorf("API key is required")
    }
    if dsp.model == "" {
        return fmt.Errorf("model is required")
    }
    return nil
}

// HealthCheck performs a health check
func (dsp *DeepSeekProvider) HealthCheck(ctx context.Context) error {
    return nil
}

Step 1.3: Update Provider Tests

File: tests/unit/providers/claude/claude_test.go

package claude_test

import (
    "context"
    "testing"
    "time"
    
    "github.com/sirupsen/logrus"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    
    "dev.helix.agent/internal/llm"
    "dev.helix.agent/internal/llm/providers"
)

func TestNewClaudeProvider(t *testing.T) {
    logger := logrus.New()
    
    t.Run("valid configuration", func(t *testing.T) {
        provider, err := providers.NewClaudeProvider(
            "test-api-key",
            "https://api.anthropic.com",
            "claude-3-opus-20240229",
            30*time.Second,
            3,
            logger,
        )
        
        require.NoError(t, err)
        require.NotNil(t, provider)
        assert.Equal(t, "claude", provider.GetProvider())
        assert.Equal(t, "claude-3-opus-20240229", provider.GetModel())
    })
    
    t.Run("missing API key", func(t *testing.T) {
        provider, err := providers.NewClaudeProvider(
            "",
            "https://api.anthropic.com",
            "claude-3-opus-20240229",
            30*time.Second,
            3,
            logger,
        )
        
        require.Error(t, err)
        require.Nil(t, provider)
        assert.Contains(t, err.Error(), "API key is required")
    })
    
    t.Run("missing model", func(t *testing.T) {
        provider, err := providers.NewClaudeProvider(
            "test-api-key",
            "https://api.anthropic.com",
            "",
            30*time.Second,
            3,
            logger,
        )
        
        require.Error(t, err)
        require.Nil(t, provider)
        assert.Contains(t, err.Error(), "model is required")
    })
}

func TestClaudeProvider_Complete(t *testing.T) {
    logger := logrus.New()
    
    provider, err := providers.NewClaudeProvider(
        "test-api-key",
        "https://api.anthropic.com",
        "claude-3-opus-20240229",
        30*time.Second,
        3,
        logger,
    )
    require.NoError(t, err)
    
    request := &llm.LLMRequest{
        Model: "claude-3-opus-20240229",
        Messages: []llm.Message{
            {
                Role:    "user",
                Content: "Hello, Claude!",
            },
        },
        MaxTokens:   100,
        Temperature: 0.7,
    }
    
    ctx := context.Background()
    response, err := provider.Complete(ctx, request)
    
    require.NoError(t, err)
    require.NotNil(t, response)
    assert.NotEmpty(t, response.ID)
    assert.Equal(t, "claude-3-opus-20240229", response.Model)
    assert.Len(t, response.Choices, 1)
    assert.Equal(t, "assistant", response.Choices[0].Message.Role)
    assert.NotEmpty(t, response.Choices[0].Message.Content)
}

func TestClaudeProvider_StreamComplete(t *testing.T) {
    logger := logrus.New()
    
    provider, err := providers.NewClaudeProvider(
        "test-api-key",
        "https://api.anthropic.com",
        "claude-3-opus-20240229",
        30*time.Second,
        3,
        logger,
    )
    require.NoError(t, err)
    
    request := &llm.LLMRequest{
        Model: "claude-3-opus-20240229",
        Messages: []llm.Message{
            {
                Role:    "user",
                Content: "Hello, Claude!",
            },
        },
        MaxTokens:   100,
        Temperature: 0.7,
    }
    
    ctx := context.Background()
    chunkChan, err := provider.StreamComplete(ctx, request)
    
    require.NoError(t, err)
    require.NotNil(t, chunkChan)
    
    // Read from channel
    chunk, ok := <-chunkChan
    assert.True(t, ok)
    assert.NotEmpty(t, chunk.ID)
    assert.Equal(t, "claude-3-opus-20240229", chunk.Model)
}

func TestClaudeProvider_GetCapabilities(t *testing.T) {
    logger := logrus.New()
    
    provider, err := providers.NewClaudeProvider(
        "test-api-key",
        "https://api.anthropic.com",
        "claude-3-opus-20240229",
        30*time.Second,
        3,
        logger,
    )
    require.NoError(t, err)
    
    capabilities := provider.GetCapabilities()
    
    assert.True(t, capabilities.Streaming)
    assert.Greater(t, capabilities.MaxTokens, 0)
    assert.True(t, capabilities.SupportsSystem)
    assert.True(t, capabilities.SupportsImages)
    assert.True(t, capabilities.SupportsTools)
    assert.Greater(t, capabilities.RateLimitRPS, 0)
    assert.NotEmpty(t, capabilities.Features)
}

func TestClaudeProvider_Validate(t *testing.T) {
    logger := logrus.New()
    
    t.Run("valid provider", func(t *testing.T) {
        provider, err := providers.NewClaudeProvider(
            "test-api-key",
            "https://api.anthropic.com",
            "claude-3-opus-20240229",
            30*time.Second,
            3,
            logger,
        )
        require.NoError(t, err)
        
        err = provider.Validate()
        assert.NoError(t, err)
    })
    
    t.Run("invalid provider - empty API key", func(t *testing.T) {
        provider := &providers.ClaudeProvider{
            apiKey: "",
            model:  "claude-3-opus-20240229",
        }
        
        err := provider.Validate()
        assert.Error(t, err)
        assert.Contains(t, err.Error(), "API key is required")
    })
}

func TestClaudeProvider_HealthCheck(t *testing.T) {
    logger := logrus.New()
    
    provider, err := providers.NewClaudeProvider(
        "test-api-key",
        "https://api.anthropic.com",
        "claude-3-opus-20240229",
        30*time.Second,
        3,
        logger,
    )
    require.NoError(t, err)
    
    ctx := context.Background()
    err = provider.HealthCheck(ctx)
    
    assert.NoError(t, err)
}

File: tests/unit/providers/deepseek/deepseek_test.go

package deepseek_test

import (
    "context"
    "testing"
    "time"
    
    "github.com/sirupsen/logrus"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
    
    "dev.helix.agent/internal/llm"
    "dev.helix.agent/internal/llm/providers"
)

func TestNewDeepSeekProvider(t *testing.T) {
    logger := logrus.New()
    
    t.Run("valid configuration", func(t *testing.T) {
        provider, err := providers.NewDeepSeekProvider(
            "test-api-key",
            "https://api.deepseek.com",
            "deepseek-chat",
            30*time.Second,
            3,
            logger,
        )
        
        require.NoError(t, err)
        require.NotNil(t, provider)
        assert.Equal(t, "deepseek", provider.GetProvider())
        assert.Equal(t, "deepseek-chat", provider.GetModel())
    })
    
    t.Run("missing API key", func(t *testing.T) {
        provider, err := providers.NewDeepSeekProvider(
            "",
            "https://api.deepseek.com",
            "deepseek-chat",
            30*time.Second,
            3,
            logger,
        )
        
        require.Error(t, err)
        require.Nil(t, provider)
        assert.Contains(t, err.Error(), "API key is required")
    })
    
    t.Run("missing model", func(t *testing.T) {
        provider, err := providers.NewDeepSeekProvider(
            "test-api-key",
            "https://api.deepseek.com",
            "",
            30*time.Second,
            3,
            logger,
        )
        
        require.Error(t, err)
        require.Nil(t, provider)
        assert.Contains(t, err.Error(), "model is required")
    })
}

func TestDeepSeekProvider_Complete(t *testing.T) {
    logger := logrus.New()
    
    provider, err := providers.NewDeepSeekProvider(
        "test-api-key",
        "https://api.deepseek.com",
        "deepseek-chat",
        30*time.Second,
        3,
        logger,
    )
    require.NoError(t, err)
    
    request := &llm.LLMRequest{
        Model: "deepseek-chat",
        Messages: []llm.Message{
            {
                Role:    "user",
                Content: "Hello, DeepSeek!",
            },
        },
        MaxTokens:   100,
        Temperature: 0.7,
    }
    
    ctx := context.Background()
    response, err := provider.Complete(ctx, request)
    
    require.NoError(t, err)
    require.NotNil(t, response)
    assert.NotEmpty(t, response.ID)
    assert.Equal(t, "deepseek-chat", response.Model)
    assert.Len(t, response.Choices, 1)
    assert.Equal(t, "assistant", response.Choices[0].Message.Role)
    assert.NotEmpty(t, response.Choices[0].Message.Content)
}

func TestDeepSeekProvider_StreamComplete(t *testing.T) {
    logger := logrus.New()
    
    provider, err := providers.NewDeepSeekProvider(
        "test-api-key",
        "https://api.deepseek.com",
        "deepseek-chat",
        30*time.Second,
        3,
        logger,
    )
    require.NoError(t, err)
    
    request := &llm.LLMRequest{
        Model: "deepseek-chat",
        Messages: []llm.Message{
            {
                Role:    "user",
                Content: "Hello, DeepSeek!",
            },
        },
        MaxTokens:   100,
        Temperature: 0.7,
    }
    
    ctx := context.Background()
    chunkChan, err := provider.StreamComplete(ctx, request)
    
    require.NoError(t, err)
    require.NotNil(t, chunkChan)
    
    chunk, ok := <-chunkChan
    assert.True(t, ok)
    assert.NotEmpty(t, chunk.ID)
    assert.Equal(t, "deepseek-chat", chunk.Model)
}

func TestDeepSeekProvider_GetCapabilities(t *testing.T) {
    logger := logrus.New()
    
    provider, err := providers.NewDeepSeekProvider(
        "test-api-key",
        "https://api.deepseek.com",
        "deepseek-chat",
        30*time.Second,
        3,
        logger,
    )
    require.NoError(t, err)
    
    capabilities := provider.GetCapabilities()
    
    assert.True(t, capabilities.Streaming)
    assert.Greater(t, capabilities.MaxTokens, 0)
    assert.True(t, capabilities.SupportsSystem)
    assert.False(t, capabilities.SupportsImages)
    assert.True(t, capabilities.SupportsTools)
    assert.Greater(t, capabilities.RateLimitRPS, 0)
    assert.NotEmpty(t, capabilities.Features)
}

func TestDeepSeekProvider_Validate(t *testing.T) {
    logger := logrus.New()
    
    t.Run("valid provider", func(t *testing.T) {
        provider, err := providers.NewDeepSeekProvider(
            "test-api-key",
            "https://api.deepseek.com",
            "deepseek-chat",
            30*time.Second,
            3,
            logger,
        )
        require.NoError(t, err)
        
        err = provider.Validate()
        assert.NoError(t, err)
    })
    
    t.Run("invalid provider - empty API key", func(t *testing.T) {
        provider := &providers.DeepSeekProvider{
            apiKey: "",
            model:  "deepseek-chat",
        }
        
        err := provider.Validate()
        assert.Error(t, err)
        assert.Contains(t, err.Error(), "API key is required")
    })
}

func TestDeepSeekProvider_HealthCheck(t *testing.T) {
    logger := logrus.New()
    
    provider, err := providers.NewDeepSeekProvider(
        "test-api-key",
        "https://api.deepseek.com",
        "deepseek-chat",
        30*time.Second,
        3,
        logger,
    )
    require.NoError(t, err)
    
    ctx := context.Background()
    err = provider.HealthCheck(ctx)
    
    assert.NoError(t, err)
}

Step 1.4: Verify Compilation

go build ./internal/llm/providers
go build ./tests/unit/providers/claude
go build ./tests/unit/providers/deepseek
go test -c ./tests/unit/providers/claude
go test -c ./tests/unit/providers/deepseek

Summary of Week 1-2 Deliverables

Week 1 Deliverables

  • Debate service types implemented
  • Advanced debate services implemented
  • Provider interface issues fixed
  • Mock provider updated

Week 2 Deliverables (Days 1-2)

  • Claude provider implemented
  • DeepSeek provider implemented
  • Provider tests updated
  • Gemini provider (Day 3)
  • Qwen provider (Day 3)
  • Zai provider (Day 4)
  • Ollama provider (Day 4)
  • Plugin error handling fixes (Day 5)

Next Steps

Continue with Day 3-5 of Week 2 to complete all provider implementations and fix plugin error handling. Then proceed to Phase 2: Test Infrastructure.

Last Updated: 2025-12-27 Version: 1.0.0